Add birdnet-pi-zach addon (zach7036/BirdNET-Pi-Enhanced-Version fork)

Creates a new Home Assistant add-on that installs from zach7036's BirdNET-Pi-Enhanced-Version fork instead of the Nachtzuster/alexbelgium fork. Leaves the existing birdnet-pi addon untouched.

Key changes from birdnet-pi:
- Installer repointed to zach7036/BirdNET-Pi-Enhanced-Version/main/newinstaller.sh
- Removed alexbelgium repo rename sed (zach7036 fork already clones itself)
- Removed merge_open_prs PR-merging step (install stable main)
- Updated slug to birdnet-pi-zach, image to birdnet-pi-zach-{arch}
- Updated README/docs upstream description
- Unique apparmor profile name

The generic Dockerfile patches (strip sudo, remap my_dir, systemctl shims) apply unchanged to the new fork since it follows standard BirdNET-Pi layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-07-10 10:30:04 +02:00
parent 17695bd396
commit 748bcc7df1
39 changed files with 3019 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
## 2026.07.10 (10-07-2026)
- Initial release: BirdNET-Pi add-on based on the zach7036/BirdNET-Pi-Enhanced-Version fork

283
birdnet-pi-zach/DOCS.md Normal file
View File

@@ -0,0 +1,283 @@
# Microphone considerations
The critical element is the microphone quality: a Boya By-lm 40 or Clippy EM272 (with a very good aux-usb converter) is key to improve the quality of detections.
Here are some example tests I did (the whole threads are really interesting also):
- <https://github.com/mcguirepr89/BirdNET-Pi/discussions/39#discussioncomment-9706951>
- <https://github.com/mcguirepr89/BirdNET-Pi/discussions/1092#discussioncomment-9706191>
**My recommendation:**
- **Best entry system (< 50€):** Boya By-lm40 (30€) + deadcat (10€)
- **Best middle end system (< 150€):** Clippy EM272 TRS/TRRS (55€) + Rode AI micro trs/trrs to usb (70€) + Rycote deadcat (27€)
- **Best high end system (<400€):** Clippy EM272 XLR (85€) or LOM Ucho Pro (75€) + Focusrite Scarlet 2i2 4th Gen (200€) + Bubblebee Pro Extreme deadcat (45€)
**Sources for high end microphones in Europe:**
- Clippy (EM272): <https://www.veldshop.nl/en/clippy-xlr-em272z1-mono-microphone.html>
- LOM (EM272): <https://store.lom.audio/collections/basicucho-series>
- Immersive sound (AOM5024): <https://immersivesoundscapes.com/earsight-standard-v2/>
# App settings recommendation
I've tested lots of settings by running 2 versions of my HA birdnet-pi addon in parallel using the same RTSP feed and comparing the impact of parameters. My conclusions aren't universal, as it seems to be highly dependent on the region and type of mic used. For example, the old model seems to be better in Australia, while the new one is better in Europe.
- **Model**
- **Version:** 6k_v2.4 _(performs better in Europe at least, the 6k performs better in Australia)_
- **Species range model:** v1 _(uncheck v2.4; seems more robust in Europe)_
- **Species occurrence threshold:** 0.001 _(was 0.00015 using v2.4; use the Species List Tester to check the correct value for you)_
- **Audio settings**
- **Default**
- **Channel:** 1 _(doesn't really matter as analysis is made on mono signal; 1 allows decreased saved audio size but seems to give slightly messed up spectrograms in my experience)_
- **Recording Length:** 18 _(that's because I use an overlap of 0.5; so it analyzes 0-3s, 2.5-5.5s, 5-8s, 7.5-10.5, 10-13, 12.5-15.5, 15-18)_
- **Extraction Length:** 9s _(could be 6, but I like to hear my birds :-))_
- **Audio format:** mp3 _(why bother with something else)_
- **Birdnet-lite settings**
- **Overlap:** 0.5s
- **Minimum confidence:** 0.7
- **Sigmoid sensitivity:** 1.25 _(I've tried 1.00 but it gave much more false positives; decreasing this value increases sensitivity)_
# Set RTSP server
Inspired by: <https://github.com/mcguirepr89/BirdNET-Pi/discussions/1006#discussioncomment-6747450>
<details>
<summary>On your desktop</summary>
- Download imager
- Install raspbian lite 64
</details>
<details>
<summary>With ssh, install requisite softwares</summary>
```bash
# Update
sudo apt-get update -y
sudo apt-get dist-upgrade -y
# Install RTSP server
sudo apt-get install -y micro ffmpeg lsof
sudo -s cd /root && wget -c https://github.com/bluenviron/mediamtx/releases/download/v1.18.1/mediamtx_v1.18.1_linux_arm64v8.tar.gz -O - | sudo tar -xz
```
</details>
<details>
<summary>Configure Audio</summary>
### Find right device
```bash
# List audio devices
arecord -l
# Check audio device parameters. Example:
arecord -D hw:1,0 --dump-hw-params
```
### Add startup script
```bash
sudo nano startmic.sh && chmod +x startmic.sh
```
Paste the following content:
```bash
#!/bin/bash
echo "Starting birdmic"
# Disable gigabit ethernet
sudo ethtool -s eth0 speed 100 duplex full autoneg on
# Detect Scarlett 2i2 card index - relevant only if you use that card
SCARLETT_INDEX=$(arecord -l | grep -i "Scarlett" | awk '{print $2}' | sed 's/://')
if [ -z "$SCARLETT_INDEX" ]; then
echo "Error: Scarlett 2i2 not found! Using 0 as default"
SCARLETT_INDEX="0"
fi
# Start mediamtx first and give it a moment to initialize
./mediamtx &
sleep 5
# Run ffmpeg
ffmpeg -nostdin -use_wallclock_as_timestamps 1 -fflags +genpts -f alsa -acodec pcm_s16be -ac 2 -ar 96000 -i plughw:$SCARLETT_INDEX,0 -ac 2 -f rtsp -acodec pcm_s16be rtsp://localhost:8554/birdmic -rtsp_transport tcp -buffer_size 512k 2>/tmp/rtsp_error &
# Set microphone volume
sleep 5
MICROPHONE_NAME="Line In 1 Gain" # for Focusrite Scarlett 2i2
sudo amixer -c 0 sset "$MICROPHONE_NAME" 40
sleep 60
# Run focusrite and autogain scripts if present
if [ -f "$HOME/focusrite.sh" ]; then
sudo python3 -u "$HOME/focusrite.sh" >/tmp/log_focusrite 2>/tmp/log_focusrite_error &
fi
if [ -f "$HOME/autogain.py" ]; then
sudo python3 -u "$HOME/autogain.py" >/tmp/log_autogain 2>/tmp/log_autogain_error &
fi
```
</details>
<details>
<summary>Optional: Startup automatically</summary>
```bash
chmod +x startmic.sh
crontab -e # select nano as your editor
```
Paste in:
```bash
@reboot $HOME/startmic.sh
```
then save and exit nano.
Reboot the Pi and test again with VLC to make sure the RTSP stream is live.
</details>
<details>
<summary>Optional: disable unnecessary elements</summary>
- **Optimize config.txt**
```bash
sudo nano /boot/firmware/config.txt
```
Paste in:
```ini
# ── Audio ──────────────────────────────────────────────────────
dtparam=audio=off
# ── Disable radios (RF + background jitter) ─────────────────────
dtoverlay=disable-wifi
dtoverlay=disable-bt
# ── Headless / minimal firmware probing ─────────────────────────
gpu_mem=16
start_x=0
camera_auto_detect=0
display_auto_detect=0
disable_splash=1
# ── Optional: only effective on newer Pi4B revs / Pi 400 ────────
arm_boost=1
hdmi_blanking=2 # Disable HDMI (save power and reduce interference)
```
- **Optimize cmdline.txt**
```bash
sudo nano /boot/firmware/cmdline.txt
```
Paste in:
```ini
console=serial0,115200 console=tty1 root=PARTUUID=2f0ecb16-02 rootfstype=ext4 fsck.repair=yes rootwait cfg80211.ieee80211_regdom=FI dwc_otg.fiq_enable=0 dwc_otg.fiq_split_enable=0 dwc_otg.fiq_fsm_enable=0 dwc_otg.lpm_enable=0 usbcore.autosuspend=-1 snd_usb_audio.nrpacks=1 threadirqs
```
- **Disable useless services**
```bash
# Disable useless services
sudo systemctl disable hciuart
sudo systemctl disable bluetooth
sudo systemctl disable triggerhappy
sudo systemctl disable avahi-daemon
sudo systemctl disable dphys-swapfile
sudo systemctl disable hciuart.service
# Disable bluetooth
for element in bluetooth btbcm hci_uart btintel btrtl btusb; do
sudo sed -i "/$element/d" /etc/modprobe.d/raspi-blacklist.conf
echo "blacklist $element" | sudo tee -a /etc/modprobe.d/raspi-blacklist.conf
done
# Disable Video (Including V4L2) on Your Raspberry Pi
for element in bcm2835_v4l2 bcm2835_codec bcm2835_isp videobuf2_vmalloc videobuf2_memops videobuf2_v4l2 videobuf2_common videodev; do
sudo sed -i "/$element/d" /etc/modprobe.d/raspi-blacklist.conf
echo "blacklist $element" | sudo tee -a /etc/modprobe.d/raspi-blacklist.conf
done
# Disable WiFi Power Management
sudo iw dev wlan0 set power_save off
for element in brcmfmac brcmutil; do
sudo sed -i "/$element/d" /etc/modprobe.d/raspi-blacklist.conf
echo "blacklist $element" | sudo tee -a /etc/modprobe.d/raspi-blacklist.conf
done
# Disable USB Power Management
echo 'on' | sudo tee /sys/bus/usb/devices/usb*/power/control
# Preventing the Raspberry Pi from Entering Power-Saving Mode
sudo apt update
sudo apt install -y cpufrequtils
echo 'GOVERNOR="performance"' | sudo tee /etc/default/cpufrequtils
sudo systemctl disable ondemand
sudo systemctl stop ondemand
```
</details>
<details>
<summary>Optional: install Focusrite driver</summary>
```bash
sudo apt-get install make linux-headers-$(uname -r)
curl -LO https://github.com/geoffreybennett/scarlett-gen2/releases/download/v6.9-v1.3/snd-usb-audio-kmod-6.6-v1.3.tar.gz
tar -xzf snd-usb-audio-kmod-6.6-v1.3.tar.gz
cd snd-usb-audio-kmod-6.6-v1.3
KSRCDIR=/lib/modules/$(uname -r)/build
make -j4 -C $KSRCDIR M=$(pwd) clean
make -j4 -C $KSRCDIR M=$(pwd)
sudo make -j4 -C $KSRCDIR M=$(pwd) INSTALL_MOD_DIR=updates/snd-usb-audio modules_install
sudo depmod
sudo reboot
dmesg | grep -A 5 -B 5 -i focusrite
```
</details>
<details>
<summary>Optional: add RAM disk</summary>
```bash
sudo cp /usr/share/systemd/tmp.mount /etc/systemd/system/tmp.mount
sudo systemctl enable tmp.mount
sudo systemctl start tmp.mount
```
</details>
<details>
<summary>Optional: Configuration for Focusrite Scarlett 2i2</summary>
Add this content in `$HOME/focusrite.sh` and run:
```bash
chmod +x "$HOME/focusrite.sh"
```
See: <https://github.com/alexbelgium/Birdnet-tools/blob/main/focusrite.sh>
</details>
<details>
<summary>Optional: Autogain script for microphone</summary>
Add this content in `$HOME/autogain.py` and run:
```bash
chmod +x "$HOME/autogain.py"
```
See: <https://github.com/alexbelgium/Birdnet-tools/blob/main/autogain.py>
</details>

241
birdnet-pi-zach/Dockerfile Normal file
View File

@@ -0,0 +1,241 @@
#============================#
# ALEXBELGIUM'S DOCKERFILE #
#============================#
# _.------.
# _.-` ('>.-`"""-.port
# '.--'` _'` _ .--.)
# -' '-.-';` `
# ' - _.' ``'--.
# '---` .-'""`
# /`
#=== Home Assistant Addon ===#
#################
# 1 Build Image #
#################
ARG BUILD_VERSION
ARG BUILD_FROM
FROM ${BUILD_FROM}
# Install locales
RUN apt-get update && apt-get install --no-install-recommends -y locales && locale-gen en_US.UTF-8
ENV DEBIAN_FRONTEND="noninteractive" \
BIRDNET_USER="pi" \
USER="pi" \
PUID=1000 \
PGID=1000 \
HOME="/home/pi" \
XDG_RUNTIME_DIR="/run/user/1000" \
PYTHON_VIRTUAL_ENV="/home/pi/BirdNET-Pi/birdnet/bin/python3" \
my_dir=/home/pi/BirdNET-Pi/scripts \
LANGUAGE=en_US:en
# Global LSIO modifications
COPY ha_lsio.sh /ha_lsio.sh
ARG CONFIGLOCATION="/config"
RUN chmod 744 /ha_lsio.sh && if grep -qr "lsio" /etc; then /ha_lsio.sh "$CONFIGLOCATION"; fi && rm /ha_lsio.sh
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# hadolint ignore=DL3015,SC2016
# Add rootfs
COPY rootfs/ /
RUN find . -type f \( -name "*.sh" -o -name "run" -o -name "finish" \) -print -exec chmod +x {} \;
RUN \
# Install dependencies
echo "Install dependencies" && \
apt-get update -y && apt-get install curl gcc python3-dev git jq sudo php-mbstring procps -y && \
\
# Correct for systemctl
mkdir -p /helpers && \
curl -f -L -s -S https://raw.githubusercontent.com/gdraheim/docker-systemctl-replacement/master/files/docker/systemctl3.py -o /helpers/systemctl && \
cp -rf /helpers/systemctl /bin/systemctl && \
chmod a+x /bin/systemctl && \
\
# Correct for journalctl
curl -f -L -s -S https://raw.githubusercontent.com/gdraheim/docker-systemctl-replacement/master/files/docker/journalctl3.py -o /helpers/journalctl && \
cp -rf /helpers/journalctl /bin/journalctl && \
chmod a+x /bin/journalctl && \
\
# Change user to pi and create /home/pi
echo "setting users" && \
if id abc >/dev/null 2>&1; then groupmod -o -g 101 abc && usermod -o -u 101 abc; fi && \
groupadd --non-unique -g 1000 "$USER" && \
useradd --non-unique --uid 1000 --gid 1000 -m "$USER" && \
\
# Ensure permissions
echo "setting permissions" && \
echo "$USER ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \
mkdir -p /home/"$USER"/.config/matplotlib && \
chown -R "$USER":"$USER" /home/"$USER" && \
git config --global --add safe.directory '*' && \
\
# Download installer
curl -f -L -s -S "https://raw.githubusercontent.com/zach7036/BirdNET-Pi-Enhanced-Version/main/newinstaller.sh" -o /newinstaller.sh && \
chmod 777 /newinstaller.sh && \
\
# Use installer to modify other scripts
#######################################
# Define file
sed -i "1a /./newinstallermod.sh" /newinstaller.sh && \
echo '#!/bin/bash' >> /newinstallermod.sh && \
# Remove all instances of sudo from all other scripts
echo 'for file in $(grep -srl "sudo" $HOME/BirdNET-Pi/scripts); do sed -i "s|sudo ||" "$file"; done' >> /newinstallermod.sh && \
echo 'for file in $(grep -srl "my_dir" $HOME/BirdNET-Pi/scripts); do sed -i "s|\$my_dir|/config|" "$file"; done' >> /newinstallermod.sh && \
# Set permission
chmod +x /newinstallermod.sh && \
\
# Modify installer
##################
# zach7036's newinstaller.sh already clones zach7036/BirdNET-Pi-Enhanced-Version, so no repo rename is needed
# Avoid rebooting at end of installation
sed -i "/reboot/d" /newinstaller.sh && \
# Use apt-get as without user action
sed -i "s|apt |apt-get |g" /newinstaller.sh && \
# Ensure chmod
sed -i "/git clone/a chown -R 1000:1000 $HOME" /newinstaller.sh && \
sed -i "/git clone/a chmod 777 \$HOME/BirdNET-Pi/scripts/*.sh" /newinstaller.sh && \
# Disable datetimectl
sed -i '/git clone/a sed -i "/CURRENT_TIMEZONE/s/$/ || true/" $HOME/BirdNET-Pi/scripts/install_birdnet.sh' /newinstaller.sh && \
# Remove all instances of sudo from the newinstaller
sed -i -e "s|== 0|== 7|g" -e "s|sudo -n true|true|g" -e "s|sudo -K|true|g" /newinstaller.sh && \
\
# Execute installer
/./newinstaller.sh && \
\
# Install dateparser and resampy, upgrade numpy
$PYTHON_VIRTUAL_ENV /usr/bin/pip3 install dateparser resampy && \
\
# Adapt for lsio usage of /app
if [ -d /app ]; then rm -r /app; fi && \
ln -s /home/"$USER" /app && \
chown -R "$USER":"$USER" /home/"$USER" /app && \
\
# Give access to caddy for files owned by the user, to allow files modification
groupmod -o -g 1000 caddy && usermod -o -u 1000 caddy && \
\
# Give access to audio devices
usermod -aG audio "$USER" && \
\
# Ensure always pi is used
grep -srl "/etc/passwd" "$HOME/BirdNET-Pi/" | while IFS= read -r file; do sed -i "s=/etc/passwd=/etc/passwd | head -1=g" "$file"; done && \
\
# Cleanup
apt-get clean all && \
rm -rf /var/lib/apt/lists/*
##################
# 2 Modify Image #
##################
# Set S6 wait time
ENV S6_CMD_WAIT_FOR_SERVICES=1 \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \
S6_SERVICES_GRACETIME=0
##################
# 3 Install apps #
##################
# Uses /bin for compatibility purposes
# hadolint ignore=DL4005
RUN if [ ! -f /bin/sh ] && [ -f /usr/bin/sh ]; then ln -s /usr/bin/sh /bin/sh; fi && \
if [ ! -f /bin/bash ] && [ -f /usr/bin/bash ]; then ln -s /usr/bin/bash /bin/bash; fi
# Modules
ARG MODULES="00-local_mounts.sh 00-smb_mounts.sh"
# Automatic modules download
COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES="alsa-utils libasound2-plugins mosquitto-clients file pulseaudio"
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh
RUN chmod 744 /ha_autoapps.sh && /ha_autoapps.sh "$PACKAGES" && rm /ha_autoapps.sh
################
# 4 Entrypoint #
################
# Add entrypoint
ENV S6_STAGE2_HOOK=/ha_entrypoint.sh
COPY ha_entrypoint.sh /ha_entrypoint.sh
RUN chmod 777 /ha_entrypoint.sh
# Install bashio
COPY bashio-standalone.sh /usr/local/lib/bashio-standalone.sh
RUN chmod 0755 /usr/local/lib/bashio-standalone.sh
# Avoid config.yaml interference
WORKDIR /config
#ENTRYPOINT ["/lib/systemd/systemd"]
#ENTRYPOINT [ "/usr/bin/env" ]
#CMD [ "/ha_entrypoint.sh" ]
#SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Allow a dockerfile independent from HA
EXPOSE 80
RUN mkdir -p /data /config
############
# 5 Labels #
############
ARG BUILD_ARCH
ARG BUILD_DATE
ARG BUILD_DESCRIPTION
ARG BUILD_NAME
ARG BUILD_REF
ARG BUILD_REPOSITORY
ARG BUILD_VERSION
ENV BUILD_VERSION="${BUILD_VERSION}"
LABEL \
io.hass.name="${BUILD_NAME}" \
io.hass.description="${BUILD_DESCRIPTION}" \
io.hass.arch="${BUILD_ARCH}" \
io.hass.type="addon" \
io.hass.version=${BUILD_VERSION} \
maintainer="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.title="${BUILD_NAME}" \
org.opencontainers.image.description="${BUILD_DESCRIPTION}" \
org.opencontainers.image.vendor="Home Assistant Add-ons" \
org.opencontainers.image.authors="alexbelgium (https://github.com/alexbelgium)" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.url="https://github.com/alexbelgium" \
org.opencontainers.image.source="https://github.com/${BUILD_REPOSITORY}" \
org.opencontainers.image.documentation="https://github.com/${BUILD_REPOSITORY}/blob/main/README.md" \
org.opencontainers.image.created=${BUILD_DATE} \
org.opencontainers.image.revision=${BUILD_REF} \
org.opencontainers.image.version=${BUILD_VERSION}
#################
# 6 Healthcheck #
#################
# Avoid spamming logs
# hadolint ignore=SC2016
RUN \
# Handle Apache configuration
if [ -d /etc/apache2/sites-available ]; then \
for file in /etc/apache2/sites-*/*.conf; do \
sed -i '/<VirtualHost/a \ \n # Match requests with the custom User-Agent "HealthCheck" \n SetEnvIf User-Agent "HealthCheck" dontlog \n # Exclude matching requests from access logs \n CustomLog ${APACHE_LOG_DIR}/access.log combined env=!dontlog' "$file"; \
done; \
fi && \
\
# Handle Nginx configuration
if [ -f /etc/nginx/nginx.conf ]; then \
awk '/http \{/{print; print "map $http_user_agent $dontlog {\n default 0;\n \"~*HealthCheck\" 1;\n}\naccess_log /var/log/nginx/access.log combined if=$dontlog;"; next}1' /etc/nginx/nginx.conf > /etc/nginx/nginx.conf.new && \
mv /etc/nginx/nginx.conf.new /etc/nginx/nginx.conf; \
fi
ENV HEALTH_PORT="80" \
HEALTH_URL=""
HEALTHCHECK \
--interval=5s \
--retries=5 \
--start-period=30s \
--timeout=25s \
CMD curl -A "HealthCheck: Docker/1.0" -s -f "http://127.0.0.1:${HEALTH_PORT}${HEALTH_URL}" &>/dev/null || exit 1

187
birdnet-pi-zach/README.md Normal file
View File

@@ -0,0 +1,187 @@
## &#9888; Open Issue : [[BirdNET-Pi Docker Standalone] Services wont start (opened 2025-06-24)](https://github.com/alexbelgium/hassio-addons/issues/1927) by [@sirtakahe](https://github.com/sirtakahe)
# Home assistant add-on: BirdNET-Pi (zach7036)
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
If this add-on saves you time or makes your setup easier, I would be very grateful for your support!
[![Buy me a coffee][donation-badge]](https://www.buymeacoffee.com/alexbelgium)
[![Donate via PayPal][paypal-badge]](https://www.paypal.com/donate/?hosted_button_id=DZFULJZTP3UQA)
## Addon informations
![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fbirdnet-pi-zach%2Fconfig.yaml)
![Ingress](https://img.shields.io/badge/dynamic/yaml?label=Ingress&query=%24.ingress&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fbirdnet-pi-zach%2Fconfig.yaml)
![Arch](https://img.shields.io/badge/dynamic/yaml?color=success&label=Arch&query=%24.arch&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fbirdnet-pi-zach%2Fconfig.yaml)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/9c6cf10bdbba45ecb202d7f579b5be0e)](https://www.codacy.com/gh/alexbelgium/hassio-addons/dashboard?utm_source=github.com&utm_medium=referral&utm_content=alexbelgium/hassio-addons&utm_campaign=Badge_Grade)
[![GitHub Super-Linter](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/weekly-supelinter.yaml?label=Lint%20code%20base)](https://github.com/alexbelgium/hassio-addons/actions/workflows/weekly-supelinter.yaml)
[![Builder](https://img.shields.io/github/actions/workflow/status/alexbelgium/hassio-addons/onpush_builder.yaml?label=Builder)](https://github.com/alexbelgium/hassio-addons/actions/workflows/onpush_builder.yaml)
[donation-badge]: https://img.shields.io/badge/Buy%20me%20a%20coffee-%23d32f2f?logo=buy-me-a-coffee&style=flat&logoColor=white
[paypal-badge]: https://img.shields.io/badge/Donate%20via%20PayPal-0070BA?logo=paypal&style=flat&logoColor=white
_Thanks to everyone having starred my repo! To star it click on the image below, then it will be on top right. Thanks!_
[![Stargazers repo roster for @alexbelgium/hassio-addons](https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.github/stars2.svg)](https://github.com/alexbelgium/hassio-addons/stargazers)
![downloads evolution](https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/birdnet-pi-zach/stats.png)
## About
_Note : For usage without HomeAssistant (classic docker container), see [here](https://github.com/alexbelgium/hassio-addons/blob/master/birdnet-pi-zach/README_standalone.md)_
---
[BirdNET-Pi](https://github.com/zach7036/BirdNET-Pi-Enhanced-Version) is an AI solution for continuous avian monitoring and identification originally developed by @mcguirepr89 on github (https://github.com/mcguirepr89/BirdNET-Pi). This add-on builds on the [BirdNET-Pi-Enhanced-Version fork by @zach7036](https://github.com/zach7036/BirdNET-Pi-Enhanced-Version), which adds new features and an updated UI. For the add-on based on @Nachtzuster's fork, use the separate `birdnet-pi` add-on instead.
Features of the addon :
- Robust base image provided by [linuxserver](https://github.com/linuxserver/docker-baseimage-debian)
- Working docker system thanks to https://github.com/gdraheim/docker-systemctl-replacement
- Uses HA pulseaudio server
- Uses HA tmpfs to store temporary files in ram and avoid disk wear
- Exposes all config files to /config to allow remanence and easy access
- Allows to modify the location of the stored bird songs (preferably to an external hdd)
- Supports ingress, to allow secure remote access without exposing ports
## Configuration
---
Install, then start the addon a first time
Webui can be found by two ways :
- Ingress from HA (no password but some functions don't work)
- Direct access with <http://homeassistant:port>, port being the one defined in the birdnet.conf. The username when asked for a password is `birdnet`, the password is the one that you can define in the birdnet.con (blank by default). This is different than the password from the addon options, which is the one that must be used to access the web terminal
Web terminal access : username `pi`, password : as defined in the addon options
You'll need a microphone : either use one connected to HA or the audio stream of a rstp camera.
Options can be configured through three ways :
- Addon options
```yaml
BIRDSONGS_FOLDER: folder to store birdsongs file # It should be an ssd if you want to avoid clogging of analysis
MQTT_DISABLED : if true, disables automatic mqtt publishing. Only valid if there is a local broker already available
LIVESTREAM_BOOT_ENABLED: start livestream from boot, or from settings
Use_tphakala_model_v2: false # switch to BirdNET-Go classifier files
PROCESSED_FOLDER_ENABLED : if enabled, you need to set in the birdnet.conf (or the setting of birdnet) the number of last wav files that will be saved in the temporary folder "/tmp/Processed" within the tmpfs (so no disk wear) in case you want to retrieve them. This amount can be adapted from the addon options
TZ: Etc/UTC specify a timezone to use, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
pi_password: set the user password to access the web terminal
localdisks: sda1 #put the hardware name of your drive to mount separated by commas, or its label. ex. sda1, sdb1, MYNAS...
networkdisks: "//SERVER/SHARE" # optional, list of smb servers to mount, separated by commas
cifsusername: "username" # optional, smb username, same for all smb shares
cifspassword: "password" # optional, smb password
cifsdomain: "domain" # optional, allow setting the domain for the smb share
```
- Config.yaml
Additional variables can be configured using the config.yaml file found in /config/db21ed7f_birdnet-pi-zach/config.yaml using the Filebrowser addon
- Config_env.yaml
Additional environment variables can be configured there
### Mounting Drives
This addon supports mounting both local drives and remote SMB shares:
- **Local drives**: See [Mounting Local Drives in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Mounting-Local-Drives-in-Addons)
- **Remote shares**: See [Mounting Remote Shares in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Mounting-remote-shares-in-Addons)
### Custom Scripts and Environment Variables
This addon supports custom scripts and environment variables through the `addon_config` mapping:
- **Custom scripts**: See [Running Custom Scripts in Addons](https://github.com/alexbelgium/hassio-addons/wiki/Running-custom-scripts-in-Addons)
- **env_vars option**: Use the add-on `env_vars` option to pass extra environment variables (uppercase or lowercase names). See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
## Installation
---
The installation of this add-on is pretty straightforward and not different in comparison to installing any other add-on.
1. Add my add-ons repository to your home assistant instance (in supervisor addons store at top right, or click button below if you have configured my HA)
[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Falexbelgium%2Fhassio-addons)
1. Install this add-on.
1. Click the `Save` button to store your configuration.
1. Set the add-on options to your preferences
1. Start the add-on.
1. Check the logs of the add-on to see if everything went well.
1. Open the webUI and adapt the software options
## Integration with HA
---
### Apprise
You can use apprise to send notifications with mqtt, then act on those using HomeAssistant
Further informations : https://wander.ingstar.com/projects/birdnetpi.html
### Automatic mqtt
If mqtt is installed, the addon automatically updates the birdnet topic with each detected species
## Using ssl
---
Option 1 : Install let's encrypt addon, generate certificates. They are by default certfile.pem and keyfile.pem stored in /ssl. Just enable ssl from the addon option and it will work.
Option 2 : enable port 80, define your BirdNET-Pi URL as https. Certificate will be automatically generated by caddy
## Improve detections
---
### Gain for card
Using alsamixer in the Terminal tab, make sure that the sound level is high enough but not too high (not in the red part)
https://github.com/mcguirepr89/BirdNET-Pi/wiki/Adjusting-your-sound-card
### Ferrite
Adding ferrite beads lead in my case to worst noise
### Aux to usb adapters
Based on my test, only adapters using KT0210 (such as Ugreen's) work. I couldn't get adapters based on ALC to be detected.
### Microphone comparison
Recommended microphones ([full discussion here](https://github.com/mcguirepr89/BirdNET-Pi/discussions/39)):
- Clippy EM272 (https://www.veldshop.nl/en/smart-clippy-em272z1-mono-omni-microphone.html) + ugreen aux to usb connector : best sensitivity with lavalier tech
- Boya By-LM40 : best quality/price
- Hyperx Quadcast : best sensitivity with cardioid tech
Conclusion, using mic from Dahua is good enough, EM272 is optimal, but Boya by-lm40 is a very good compromise as birndet model analysis the 0-15000Hz range
![image](https://github.com/alexbelgium/hassio-addons/assets/44178713/df992b79-7171-4f73-b0c0-55eb4256cd5b)
### Denoise ([Full discussion here](https://github.com/mcguirepr89/BirdNET-Pi/discussions/597))
Denoise is frowned upon by serious researchers. However it does seem to significantly increase quality of detection ! Here is how to do it in HA :
- Using Portainer addon, go in the hassio_audio container, and modify the file /etc/pulse/system.pa to add the line `load-module module-echo-cancel`
- Go in the Terminal addon, and type `ha audio restart`
- Select the echo cancelled device as input device in the addon options
### High pass
Should be avoided as the model uses the whole 0-15khz range
## Common issues
Not yet available
## Support
Create an issue on github
---
![illustration](https://raw.githubusercontent.com/tphakala/birdnet-pi/main/doc/birdnet-pi-dashboard.webp)

View File

@@ -0,0 +1,139 @@
# BirdNET-Pi Docker Installation Guide
I maintain this and other Home Assistant add-ons in my free time: keeping up with upstream changes, HA changes, and testing on real hardware takes a lot of time (and some money). I use around 5-10 of my >110 addons so regularly I install test machines (and purchase some test services such as vpn) that I don't use myself to troubleshoot and improve the addons
If this add-on saves you time or makes your setup easier, I would be very grateful for your support!
[![Buy me a coffee][donation-badge]](https://www.buymeacoffee.com/alexbelgium)
[![Donate via PayPal][paypal-badge]](https://www.paypal.com/donate/?hosted_button_id=DZFULJZTP3UQA)
## Addon informations
[donation-badge]: https://img.shields.io/badge/Buy%20me%20a%20coffee-%23d32f2f?logo=buy-me-a-coffee&style=flat&logoColor=white
[paypal-badge]: https://img.shields.io/badge/Donate%20via%20PayPal-0070BA?logo=paypal&style=flat&logoColor=white
This guide provides instructions on how to install and run the BirdNET-Pi container using Docker Compose without dependency on HomeAssistant.
_Note : For usage as an HomeAssistant addon, see [here](https://github.com/alexbelgium/hassio-addons/blob/master/birdnet-pi-zach/README.md)_
Thanks to @gotschi for the initial Docker Compose
## Prerequisites
Ensure you have the following installed on your system:
- [Docker](https://docs.docker.com/get-docker/)
- [Docker Compose](https://docs.docker.com/compose/install/)
## Installation
1. **Create a directory for BirdNET-Pi**
```sh
mkdir -p ~/birdnet-pi-zach && cd ~/birdnet-pi-zach
```
2. **Create a `docker-compose.yml` file**
Create and open the file with:
```sh
nano docker-compose.yml
```
Copy and paste the following configuration:
```yaml
services:
birdnet-pi-zach:
container_name: birdnet-pi-zach
image: ghcr.io/alexbelgium/birdnet-pi-zach-amd64:latest # or ghcr.io/alexbelgium/birdnet-pi-zach-aarch64:latest depending on your system
restart: unless-stopped
ports:
- "8001:8081" # Used to access WebUI
- "80:80" # Optional: set to 80 to use Caddy's automatic SSL. Can otherwise be set to null to avoid opening an additional port
environment:
- TZ=Europe/Vienna # Optional: Set your timezone according to https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- BIRDSONGS_FOLDER=/config/BirdSongs # Folder to store bird songs, be sure to use a path that is mapped to a volume (such as /config)
- LIVESTREAM_BOOT_ENABLED=false # Enable/disable livestream on boot
- Use_tphakala_model_v2=false # Switch to BirdNET-Go classifier files
- ssl=false # Enable/disable SSL
- certfile=fullchain.pem # SSL certificate file (located in /ssl/)
- keyfile=privkey.pem # SSL key file (located in /ssl/)
- pi_password= # Optional: Set web terminal password for user `pi`
- MQTT_HOST_manual= # Optional: Manual MQTT host
- MQTT_PASSWORD_manual= # Optional: Manual MQTT password
- MQTT_PORT_manual= # Optional: Manual MQTT port
- MQTT_USER_manual= # Optional: Manual MQTT user
- PULSE_SERVER=unix:/tmp/pulseaudio.socket
- PULSE_COOKIE=/tmp/pulseaudio.cookie
volumes:
- ./config:/config # All your configuration files - and location of the default Birdsongs folder
- ./ssl:/ssl # SSL certificates
- /dev/shm:/dev/shm # Shared memory
- /tmp/pulseaudio.socket:/tmp/pulseaudio.socket
- /tmp/pulseaudio.client.conf:/etc/pulse/client.conf
devices:
- /dev/snd:/dev/snd
group_add:
- audio
tmpfs:
- /tmp # Optional
```
3. **Start the Container**
Run the following command in the same directory as `docker-compose.yml`:
```sh
docker compose up -d
```
This will start the BirdNET-Pi container in detached mode.
4. **Access BirdNET-Pi Web UI**
Open your browser and navigate to:
```sh
http://localhost:8001 # Or whatever port you have configured
```
Replace `localhost` with your server's IP address if running on another machine.
When prompted for credentials in the web terminal, use the username `pi` and the password defined by `pi_password` (blank if unset).
## troubleshoot
If rtsp feed doesn't work, perhaps you need to add "-rtsp-transport tcp" to your ffmpeg instruction, or allow udp on your network
## Updating to the Latest Version
To check for new versions of the container and update:
1. **Check for the latest version**
Visit the container registry:
[https://github.com/alexbelgium/hassio-addons/pkgs/container/birdnet-pi-zach-amd64](https://github.com/alexbelgium/hassio-addons/pkgs/container/birdnet-pi-zach-amd64)
The latest version tag (e.g., `2025.02.23`) will be listed.
2. **Update and restart the container**
Run the following commands:
```sh
docker compose pull birdnet-pi-zach
docker compose up -d --force-recreate
```
This pulls the latest image and restarts the container.
3. **Verify the update**
```sh
docker images | grep birdnet-pi-zach
```
This will show the latest downloaded image version.
## Stopping and Removing the Container
To stop and remove the container, run:
```sh
docker compose down
```
This will stop and remove BirdNET-Pi while keeping the configuration and recorded songs intact.
---
Now you're all set to enjoy BirdNET-Pi with Docker! 🐦

View File

@@ -0,0 +1,67 @@
#include <tunables/global>
profile birdnet-pi-zach_addon flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
capability chown,
capability dac_override,
capability dac_read_search,
capability fowner,
capability setgid,
capability setuid,
capability sys_chroot,
capability sys_admin,
file,
signal,
mount,
umount,
remount,
network udp,
network tcp,
network dgram,
network stream,
network inet,
network inet6,
network netlink raw,
network unix dgram,
# S6-Overlay
/init ix,
/run/{s6,s6-rc*,service}/** ix,
/package/** ix,
/command/** ix,
/run/{,**} rwk,
/dev/tty rw,
/bin/** ix,
/usr/bin/** ix,
/usr/lib/bashio/** ix,
/etc/s6/** rix,
/run/s6/** rix,
/etc/services.d/** rwix,
/etc/cont-init.d/** rwix,
/etc/cont-finish.d/** rwix,
/init rix,
/var/run/** mrwkl,
/var/run/ mrwkl,
/dev/i2c-1 mrwkl,
# Files required
/dev/fuse mrwkl,
/dev/sda1 mrwkl,
/dev/sdb1 mrwkl,
/dev/nvme0 mrwkl,
/dev/nvme1 mrwkl,
/dev/mmcblk0p1 mrwkl,
/dev/* mrwkl,
/tmp/** mrkwl,
# Data access
/data/** rw,
# suppress ptrace denials when using 'docker ps' or using 'ps' inside a container
ptrace (trace,read) peer=docker-default,
# docker daemon confinement requires explict allow rule for signal
signal (receive) set=(kill,term) peer=/usr/bin/docker,
}

View File

@@ -0,0 +1,4 @@
---
build_from:
aarch64: ghcr.io/linuxserver/baseimage-debian:arm64v8-bookworm
amd64: ghcr.io/linuxserver/baseimage-debian:amd64-bookworm

120
birdnet-pi-zach/config.yaml Normal file
View File

@@ -0,0 +1,120 @@
arch:
- aarch64
- amd64
audio: true
backup: cold
description: Realtime acoustic bird classification system
devices:
- /dev/dri
- /dev/dri/card0
- /dev/dri/card1
- /dev/dri/renderD128
- /dev/vchiq
- /dev/video10
- /dev/video11
- /dev/video12
- /dev/video13
- /dev/video14
- /dev/video15
- /dev/video16
- /dev/ttyUSB0
- /dev/sda
- /dev/sdb
- /dev/sdc
- /dev/sdd
- /dev/sde
- /dev/sdf
- /dev/sdg
- /dev/nvme
- /dev/nvme0
- /dev/nvme0n1
- /dev/nvme0n1p1
- /dev/nvme0n1p2
- /dev/mmcblk
- /dev/fuse
- /dev/sda1
- /dev/sdb1
- /dev/sdc1
- /dev/sdd1
- /dev/sde1
- /dev/sdf1
- /dev/sdg1
- /dev/sda2
- /dev/sdb2
- /dev/sdc2
- /dev/sdd2
- /dev/sde2
- /dev/sdf2
- /dev/sdg2
- /dev/sda3
- /dev/sdb3
- /dev/sda4
- /dev/sdb4
- /dev/sda5
- /dev/sda6
- /dev/sda7
- /dev/sda8
- /dev/nvme0
- /dev/nvme1
- /dev/nvme2
image: ghcr.io/alexbelgium/birdnet-pi-zach-{arch}
ingress: true
init: false
map:
- addon_config:rw
- media:rw
- share:rw
- ssl
name: BirdNET-Pi (zach7036)
options:
env_vars: []
BIRDSONGS_FOLDER: /config/BirdSongs
LIVESTREAM_BOOT_ENABLED: false
MQTT_DISABLED: true
TZ: Europe/Paris
Use_tphakala_model_v2: false
certfile: fullchain.pem
keyfile: privkey.pem
ssl: false
panel_admin: false
panel_icon: mdi:bird
ports:
80/tcp: null
8081/tcp: 8081
ports_description:
80/tcp: "Optional : set to 80 to use caddy's automatic ssl"
8081/tcp: Web ui
privileged:
- SYS_ADMIN
- DAC_READ_SEARCH
schema:
env_vars:
- name: match(^[A-Za-z0-9_]+$)
value: str?
BIRDSONGS_FOLDER: str?
LIVESTREAM_BOOT_ENABLED: bool
MQTT_DISABLED: bool?
MQTT_HOST_manual: str?
MQTT_PASSWORD_manual: password?
MQTT_PORT_manual: int?
MQTT_USER_manual: str?
TZ: str?
Use_tphakala_model_v2: bool
certfile: str
cifsdomain: str?
cifspassword: str?
cifsusername: str?
keyfile: str
localdisks: str?
networkdisks: str?
pi_password: password
ssl: bool
services:
- mqtt:want
slug: birdnet-pi-zach
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pi-zach
usb: true
version: 2026.07.10
video: true

BIN
birdnet-pi-zach/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
birdnet-pi-zach/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,15 @@
#!/usr/bin/with-contenv bash
# shellcheck shell=bash
# Correct /config permissions after startup
chown pi:pi /config
# Waiting for dbus
until [[ -e /var/run/dbus/system_bus_socket ]]; do
sleep 1s
done
TZ_VALUE="$(timedatectl show -p Timezone --value)"
export TZ="$TZ_VALUE"
echo "Starting service: php pfm"
exec /usr/sbin/php-fpm* -F

View File

@@ -0,0 +1,13 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# Waiting for dbus
until [[ -e /var/run/dbus/system_bus_socket ]]; do
sleep 1s
done
TZ_VALUE="$(timedatectl show -p Timezone --value)"
export TZ="$TZ_VALUE"
echo "Starting service: avahi daemon"
exec \
avahi-daemon --no-chroot

View File

@@ -0,0 +1,25 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# Dependencies
sockfile="empty"
until [[ -e /var/run/dbus/system_bus_socket ]] && [[ -e "$sockfile" ]]; do
sleep 1s
sockfile="$(find /run/php -name "*.sock")"
done
# Correct fpm.sock
chown caddy:caddy /run/php/php*-fpm.sock
sed -i "s|/run/php/php-fpm.sock|$sockfile|g" /helpers/caddy_ingress.sh
sed -i "s|/run/php/php-fpm.sock|$sockfile|g" /etc/caddy/Caddyfile
sed -i "s|/run/php/php-fpm.sock|$sockfile|g" "$HOME"/BirdNET-Pi/scripts/update_caddyfile.sh
# Set timezone
TZ_VALUE="$(timedatectl show -p Timezone --value)"
export TZ="$TZ_VALUE"
# Update caddyfile with password
/."$HOME"/BirdNET-Pi/scripts/update_caddyfile.sh &> /dev/null || true
echo "Starting service: caddy"
/usr/bin/caddy run --config /etc/caddy/Caddyfile

View File

@@ -0,0 +1,10 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Set timezone
TZ_VALUE="$(timedatectl show -p Timezone --value)"
export TZ="$TZ_VALUE"
echo "Starting service: nginx"
nginx

View File

@@ -0,0 +1,282 @@
#!/usr/bin/env bash
# shellcheck shell=bash
# Improved BirdNET-Pi Monitoring Script with Recovery Alerts and Condensed Logs
HOME="/home/pi"
########################################
# Logging Functions (color-coded for terminal clarity)
########################################
log_green() { echo -e "\033[32m$1\033[0m"; }
log_red() { echo -e "\033[31m$1\033[0m"; }
log_yellow() { echo -e "\033[33m$1\033[0m"; }
log_blue() { echo -e "\033[34m$1\033[0m"; }
########################################
# Read configuration
########################################
set +u
# shellcheck disable=SC1091
source /etc/birdnet/birdnet.conf
########################################
# Wait 5 minutes for system stabilization
########################################
sleep 5m
log_green "Starting service: throttlerecording"
########################################
# Define Directories, Files, and Constants
########################################
INGEST_DIR="$(readlink -f "$HOME/BirdSongs/StreamData")"
ANALYZING_NOW_FILE="$INGEST_DIR/analyzing_now.txt"
touch "$ANALYZING_NOW_FILE"
BIRDSONGS_DIR="$(readlink -f "$HOME/BirdSongs/Extracted/By_Date")"
# Ensure directories and set permissions
mkdir -p "$INGEST_DIR" || {
log_red "Failed to create directory: $INGEST_DIR"
exit 1
}
chown -R pi:pi "$INGEST_DIR" || log_yellow "Could not change ownership for $INGEST_DIR"
chmod -R 755 "$INGEST_DIR" || log_yellow "Could not set permissions for $INGEST_DIR"
# Services to monitor
SERVICES=(birdnet_analysis chart_viewer spectrogram_viewer birdnet_recording birdnet_log birdnet_stats)
########################################
# Notification settings
########################################
NOTIFICATION_INTERVAL=1800 # 30 minutes in seconds
NOTIFICATION_INTERVAL_IN_MINUTES=$((NOTIFICATION_INTERVAL / 60))
last_notification_time=0
issue_reported=0 # 1 = an issue was reported, 0 = system is normal
declare -A SERVICE_INACTIVE_COUNT=()
# Disk usage threshold (percentage)
DISK_USAGE_THRESHOLD=95
# "Analyzing" file check variables
same_file_counter=0
SAME_FILE_THRESHOLD=2
if [[ -f "$ANALYZING_NOW_FILE" ]]; then
analyzing_now=$(< "$ANALYZING_NOW_FILE")
else
analyzing_now=""
fi
########################################
# Notification Functions
########################################
apprisealert() {
local issue_message="$1"
local current_time
current_time=$(date +%s)
# Calculate time_diff in minutes since last notification
local time_diff=$(((current_time - last_notification_time) / 60))
# Throttle notifications
if ((time_diff < NOTIFICATION_INTERVAL_IN_MINUTES)); then
log_yellow "Notification suppressed (last sent ${time_diff} minutes ago)."
return
fi
local stopped_service="<br><b>Stopped services:</b> "
for service in "${SERVICES[@]}"; do
if [[ "$(systemctl is-active "$service")" != "active" ]]; then
stopped_service+="$service; "
fi
done
local notification="<b>Issue:</b> $issue_message"
notification+="$stopped_service"
notification+="<br><b>System:</b> ${SITE_NAME:-$(hostname)}"
notification+="<br>Available disk space: $(df -h "$BIRDSONGS_DIR" | awk 'NR==2 {print $4}')"
notification+="<br>----Last log lines----"
notification+="<br> $(timeout 15 cat /proc/1/fd/1 | head -n 5)"
notification+="<br>----------------------"
[[ -n "$BIRDNETPI_URL" ]] && notification+="<br><a href=\"$BIRDNETPI_URL\">Access your BirdNET-Pi</a>"
local TITLE="BirdNET-Analyzer Alert"
if [[ -f "$HOME/BirdNET-Pi/birdnet/bin/apprise" && -s "$HOME/BirdNET-Pi/apprise.txt" ]]; then
"$HOME/BirdNET-Pi/birdnet/bin/apprise" -vv -t "$TITLE" -b "$notification" \
--input-format=html --config="$HOME/BirdNET-Pi/apprise.txt"
last_notification_time=$current_time
issue_reported=1
else
log_red "Apprise not configured or missing!"
fi
}
apprisealert_recovery() {
# Only send a recovery message if we had previously reported an issue
if ((issue_reported == 1)); then
log_green "$(date) INFO: System is back to normal. Sending recovery notification."
local TITLE="BirdNET-Pi System Recovered"
local notification="<b>All monitored services are back to normal.</b><br>"
notification+="<b>System:</b> ${SITE_NAME:-$(hostname)}<br>"
notification+="Available disk space: $(df -h "$BIRDSONGS_DIR" | awk 'NR==2 {print $4}')"
if [[ -f "$HOME/BirdNET-Pi/birdnet/bin/apprise" && -s "$HOME/BirdNET-Pi/apprise.txt" ]]; then
"$HOME/BirdNET-Pi/birdnet/bin/apprise" -vv -t "$TITLE" -b "$notification" \
--input-format=html --config="$HOME/BirdNET-Pi/apprise.txt"
fi
issue_reported=0
fi
}
########################################
# Helper Checks
########################################
check_disk_space() {
local current_usage
current_usage=$(df -h "$BIRDSONGS_DIR" | awk 'NR==2 {print $5}' | sed 's/%//')
if ((current_usage >= DISK_USAGE_THRESHOLD)); then
log_red "$(date) INFO: Disk usage is at ${current_usage}% (CRITICAL!)"
apprisealert "Disk usage critical: ${current_usage}%"
return 1
else
log_green "$(date) INFO: Disk usage is within acceptable limits (${current_usage}%)."
return 0
fi
}
check_analyzing_now() {
local current_file
current_file=$(cat "$ANALYZING_NOW_FILE" 2> /dev/null)
if [[ "$current_file" == "$analyzing_now" ]]; then
((same_file_counter++))
else
same_file_counter=0
analyzing_now="$current_file"
fi
if ((same_file_counter >= SAME_FILE_THRESHOLD)); then
log_red "$(date) INFO: 'analyzing_now' file unchanged for $SAME_FILE_THRESHOLD iterations."
apprisealert "No change in analyzing_now for ${SAME_FILE_THRESHOLD} iterations"
"$HOME/BirdNET-Pi/scripts/restart_services.sh"
same_file_counter=0
return 1
else
# Only log if it changed this iteration
if ((same_file_counter == 0)); then
log_green "$(date) INFO: 'analyzing_now' file has been updated."
fi
return 0
fi
}
check_queue() {
local wav_count
wav_count=$(find -L "$INGEST_DIR" -maxdepth 1 -name '*.wav' | wc -l)
log_green "$(date) INFO: Queue is at a manageable level (${wav_count} wav files)."
if ((wav_count > 50)); then
log_red "$(date) INFO: Queue >50. Stopping recorder + restarting analyzer."
apprisealert "Queue exceeded 50: stopping recorder, restarting analyzer."
sudo systemctl stop birdnet_recording
sudo systemctl restart birdnet_analysis
return 1
elif ((wav_count > 30)); then
log_red "$(date) INFO: Queue >30. Restarting analyzer."
apprisealert "Queue exceeded 30: restarting analyzer."
sudo systemctl restart birdnet_analysis
return 1
fi
return 0
}
check_services() {
local any_inactive=0
for service in "${SERVICES[@]}"; do
if [[ "$(systemctl is-active "$service")" != "active" ]]; then
SERVICE_INACTIVE_COUNT["$service"]=$((SERVICE_INACTIVE_COUNT["$service"] + 1))
if ((SERVICE_INACTIVE_COUNT["$service"] == 1)); then
# First time inactive => Try to start
log_yellow "$(date) INFO: Service '$service' is inactive. Attempting to start..."
systemctl start "$service"
any_inactive=1
elif ((SERVICE_INACTIVE_COUNT["$service"] == 2)); then
# Second consecutive time => Send an alert
log_red "$(date) INFO: Service '$service' is still inactive after restart attempt."
apprisealert "Service '$service' remains inactive after restart attempt."
any_inactive=1
else
# Beyond second check => keep logging or do advanced actions
log_red "$(date) INFO: Service '$service' inactive for ${SERVICE_INACTIVE_COUNT["$service"]} checks in a row."
any_inactive=1
fi
else
# Service is active => reset counter
if ((SERVICE_INACTIVE_COUNT["$service"] > 0)); then
log_green "$(date) INFO: Service '$service' is back to active. Resetting counter."
fi
SERVICE_INACTIVE_COUNT["$service"]=0
fi
done
if ((any_inactive == 0)); then
log_green "$(date) INFO: All services are active"
return 0
else
log_red "$(date) INFO: One or more services are inactive"
return 1
fi
}
check_for_empty_stream() {
local log_tail
log_tail=$(timeout 15 cat /proc/1/fd/1 | tail -n 5)
if echo "$log_tail" | grep -q "Haliastur indus"; then
log_red "$(date) INFO: Potential empty stream detected (frequent 'Haliastur indus')."
apprisealert "Potential empty stream detected — frequent 'Haliastur indus' in log"
return 1
fi
return 0
}
########################################
# Main Monitoring Loop
########################################
TZ_VALUE="$(timedatectl show -p Timezone --value)"
export TZ="$TZ_VALUE"
while true; do
sleep 61
log_blue "----------------------------------------"
log_blue "$(date) INFO: Starting monitoring check"
any_issue=0
# 1) Disk usage
check_disk_space || any_issue=1
# 2) 'analyzing_now' file
check_analyzing_now || any_issue=1
# 3) Queue check
check_queue || any_issue=1
# 4) Services check
check_services || any_issue=1
# 5) Check for potential empty stream
check_for_empty_stream || any_issue=1
# Final summary
if ((any_issue == 0)); then
log_green "$(date) INFO: All systems are functioning normally"
apprisealert_recovery
else
log_red "$(date) INFO: Issues detected. System status is not fully operational."
fi
log_blue "----------------------------------------"
done

View File

@@ -0,0 +1,51 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# Maximum file size in bytes (50MB)
MAX_SIZE=$((50 * 1024 * 1024))
# Function to check if a file is a valid WAV
is_valid_wav() {
local file="$1"
# Check if the file contains a valid WAV header
file "$file" | grep -qE 'WAVE audio'
}
if [ -d "$HOME"/BirdSongs/StreamData ]; then
bashio::log.fatal "Container stopping, saving temporary files."
# Stop the services in parallel
if systemctl is-active --quiet birdnet_analysis; then
bashio::log.info "Stopping birdnet_analysis service."
systemctl stop birdnet_analysis &
fi
if systemctl is-active --quiet birdnet_recording; then
bashio::log.info "Stopping birdnet_recording service."
systemctl stop birdnet_recording &
fi
# Wait for both services to stop
wait
# Create the destination directory
mkdir -p /config/TemporaryFiles
# Move only valid WAV files under 50MB
shopt -s nullglob # Prevent errors if no files match
for file in "$HOME"/BirdSongs/StreamData/*.wav; do
if [ -f "$file" ] && [ "$(stat --format="%s" "$file")" -lt "$MAX_SIZE" ] && is_valid_wav "$file"; then
if mv -v "$file" /config/TemporaryFiles/; then
bashio::log.info "Moved valid WAV file: $(basename "$file")"
else
bashio::log.error "Failed to move: $(basename "$file")"
fi
else
bashio::log.warning "Skipping invalid or large file: $(basename "$file")"
fi
done
bashio::log.info "... files safe, allowing container to stop."
else
bashio::log.info "No StreamData directory to process."
fi

View File

@@ -0,0 +1,126 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
set -e
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
###############
# SET /CONFIG #
###############
bashio::log.info "Ensuring the file structure is correct:"
# Create default configuration files if not present
echo "... creating default files"
DEFAULT_FILES=("body.txt" "apprise.txt" "exclude_species_list.txt" "IdentifiedSoFar.txt" "disk_check_exclude.txt" "confirmed_species_list.txt" "blacklisted_images.txt" "whitelist_species_list.txt")
for file in "${DEFAULT_FILES[@]}"; do
if [ ! -f "/config/$file" ]; then
echo "" > "/config/$file"
fi
done
touch /config/include_species_list.txt # Ensure this is always created
# Set BirdSongs folder location from configuration if specified
BIRDSONGS_FOLDER="/config/BirdSongs"
if bashio::config.has_value "BIRDSONGS_FOLDER"; then
BIRDSONGS_FOLDER_OPTION="$(bashio::config "BIRDSONGS_FOLDER")"
echo "... BIRDSONGS_FOLDER set to $BIRDSONGS_FOLDER_OPTION"
mkdir -p "$BIRDSONGS_FOLDER_OPTION" || bashio::log.fatal "...... folder couldn't be created"
if [ -d "$BIRDSONGS_FOLDER_OPTION" ]; then
if [ "$(stat -c '%u:%g' "$BIRDSONGS_FOLDER_OPTION")" != "$(id -u pi):$(id -g pi)" ]; then
chown -R pi:pi "$BIRDSONGS_FOLDER_OPTION" || bashio::log.fatal "...... folder couldn't be given permissions for $(id -u pi):$(id -g pi)"
fi
BIRDSONGS_FOLDER="$BIRDSONGS_FOLDER_OPTION"
else
bashio::log.warning "BIRDSONGS_FOLDER reverted to /config/BirdSongs"
fi
fi
# Create default folders
echo "... creating default folders; it is highly recommended to store these on an SSD"
mkdir -p "$BIRDSONGS_FOLDER/By_Date" "$BIRDSONGS_FOLDER/Charts"
# Use tmpfs if installed
if df -T /tmp | grep -q "tmpfs"; then
echo "... tmpfs detected, using it for StreamData and Processed to reduce disk wear"
mkdir -p /tmp/StreamData /tmp/Processed
[ -d "$HOME/BirdSongs/StreamData" ] && rm -r "$HOME/BirdSongs/StreamData"
[ -d "$HOME/BirdSongs/Processed" ] && rm -r "$HOME/BirdSongs/Processed"
sudo -u pi ln -fs /tmp/StreamData "$HOME/BirdSongs/StreamData"
sudo -u pi ln -fs /tmp/Processed "$HOME/BirdSongs/Processed"
fi
# Set permissions for created files and folders
echo "... setting permissions for user pi"
chown -R pi:pi /config /etc/birdnet "$BIRDSONGS_FOLDER" /tmp
chmod -R 755 /config /etc/birdnet "$BIRDSONGS_FOLDER" /tmp
# Backup default birdnet.conf for sanity check
cp "$HOME/BirdNET-Pi/birdnet.conf" "$HOME/BirdNET-Pi/birdnet.bak"
# Create default birdnet.conf if not existing
if [ ! -f /config/birdnet.conf ]; then
cp -f "$HOME/BirdNET-Pi/birdnet.conf" /config/
fi
# Create default birds.db
if [ ! -f /config/birds.db ]; then
echo "... creating initial db"
"$HOME/BirdNET-Pi/scripts/createdb.sh"
cp "$HOME/BirdNET-Pi/scripts/birds.db" /config/
elif [ "$(stat -c%s /config/birds.db)" -lt 10240 ]; then
echo "... your db is corrupted, creating new one"
rm /config/birds.db
"$HOME/BirdNET-Pi/scripts/createdb.sh"
cp "$HOME/BirdNET-Pi/scripts/birds.db" /config/
fi
# Symlink configuration files
echo "... creating symlinks for configuration files"
CONFIG_FILES=("$HOME/BirdNET-Pi/body.txt" "$HOME/BirdNET-Pi/birdnet.conf" "$HOME/BirdNET-Pi/scripts/whitelist_species_list.txt" "$HOME/BirdNET-Pi/blacklisted_images.txt" "$HOME/BirdNET-Pi/scripts/birds.db" "$HOME/BirdNET-Pi/BirdDB.txt" "$HOME/BirdNET-Pi/scripts/disk_check_exclude.txt" "$HOME/BirdNET-Pi/apprise.txt" "$HOME/BirdNET-Pi/exclude_species_list.txt" "$HOME/BirdNET-Pi/include_species_list.txt" "$HOME/BirdNET-Pi/IdentifiedSoFar.txt" "$HOME/BirdNET-Pi/scripts/confirmed_species_list.txt")
for file in "${CONFIG_FILES[@]}"; do
filename="${file##*/}"
[ ! -f "/config/$filename" ] && touch "/config/$filename"
[ -e "$file" ] && rm "$file"
sudo -u pi ln -fs "/config/$filename" "$file"
sudo -u pi ln -fs "/config/$filename" "$HOME/BirdNET-Pi/$filename"
sudo -u pi ln -fs "/config/$filename" "$HOME/BirdNET-Pi/scripts/$filename"
sudo -u pi ln -fs "/config/$filename" "/etc/birdnet/$filename"
done
# Create thisrun.txt for legacy modes
cp "$HOME"/BirdNET-Pi/birdnet.conf "$HOME"/BirdNET-Pi/scripts/thisrun.txt
touch "$HOME"/BirdNET-Pi/scripts/lastrun.txt
chown pi:pi "$HOME"/BirdNET-Pi/scripts/thisrun.txt
chown pi:pi "$HOME"/BirdNET-Pi/scripts/lastrun.txt
# Symlink
chown pi:pi /usr/local/bin/*
# Symlink BirdSongs folders
for folder in By_Date Charts; do
echo "... creating symlink for $BIRDSONGS_FOLDER/$folder"
[ -d "$HOME/BirdSongs/Extracted/${folder:?}" ] && rm -r "$HOME/BirdSongs/Extracted/$folder"
sudo -u pi ln -fs "$BIRDSONGS_FOLDER/$folder" "$HOME/BirdSongs/Extracted/$folder"
done
# Set permissions for newly created files and folders
echo "... checking and setting permissions"
chmod -R 755 /config/*
chmod 755 /config
# Create folder for matplotlib
echo "... setting up Matplotlabdir"
mkdir -p "$HOME"/.cache/matplotlib
chown -R "pi:pi" "$HOME"/.cache/matplotlib
chmod 755 "$HOME"/.cache/matplotlib

View File

@@ -0,0 +1,50 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
set -e
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
######################
# RESTORE STREAMDATA #
######################
if [ -d /config/TemporaryFiles ]; then
# Check if there are .wav files in /config/TemporaryFiles
if find /config/TemporaryFiles -type f -name "*.wav" | grep -q .; then
bashio::log.warning "Container was stopped while files were still being analyzed."
echo "... restoring .wav files from /config/TemporaryFiles to $HOME/BirdSongs/StreamData."
# Create the destination directory if it does not exist
mkdir -p "$HOME"/BirdSongs/StreamData
# Count the number of .wav files to be moved
file_count=$(find /config/TemporaryFiles -type f -name "*.wav" | wc -l)
echo "... found $file_count .wav files to restore."
# Move the .wav files using `mv` to avoid double log entries
mv -v /config/TemporaryFiles/*.wav "$HOME"/BirdSongs/StreamData/
# Update permissions only if files were moved successfully
if [ "$file_count" -gt 0 ]; then
chown -R pi:pi "$HOME"/BirdSongs/StreamData
fi
echo "... $file_count files restored successfully."
else
echo "... no .wav files found to restore."
fi
# Clean up the source folder if it is empty
rm -r /config/TemporaryFiles
fi

View File

@@ -0,0 +1,75 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
set -e
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
######################
# CHECK BIRDNET.CONF #
######################
bashio::log.info "Checking your birdnet.conf file integrity"
# Set variables
configcurrent="$HOME"/BirdNET-Pi/birdnet.conf
configtemplate="$HOME"/BirdNET-Pi/birdnet.bak
# Ensure both files exist before proceeding
if [ ! -f "$configcurrent" ] || [ ! -f "$configtemplate" ]; then
bashio::log.fatal "Missing required birdnet.conf or birdnet.bak file. Please ensure both are present."
exit 1
fi
# Extract variable names from config template and read each one
grep -o '^[^#=]*=' "$configtemplate" | sed 's/=//' | while read -r var; do
# Check if the variable is in configcurrent, if not, append it
if ! grep -q "^$var=" "$configcurrent"; then
bashio::log.warning "...$var was missing from your birdnet.conf file, it was re-added"
grep "^$var=" "$configtemplate" >> "$configcurrent"
fi
# Check for duplicates
if [ "$(grep -c "^$var=" "$configcurrent")" -gt 1 ]; then
bashio::log.error "Duplicate variable $var found in $configcurrent, all were commented out except for the first one"
sed -i "0,/^$var=/!s/^$var=/#$var=/" "$configcurrent"
fi
done
##############
# CHECK PORT #
##############
if [[ "$(bashio::addon.port "80")" == 3000 ]]; then
bashio::log.fatal "This is crazy but your port is set to 3000 and streamlit doesn't accept this port! You need to change it from the addon options and restart. Thanks"
sleep infinity
fi
##################
# PERFORM UPDATE #
##################
bashio::log.info "Performing potential updates"
# Adapt update_birdnet_snippets
sed -i "s|systemctl list-unit-files|false \&\& echo|g" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Avoid systemctl
sed -i "/systemctl /d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Avoid systemctl
sed -i "/install_tmp_mount/d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Use HA tmp
sed -i "/find /d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Not useful
sed -i "/set -x/d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Not useful
sed -i "/restart_services/d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Not useful
sed -i "s|/etc/birdnet/birdnet.conf|/config/birdnet.conf|g" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh
sed -i "/update_caddyfile/c echo \"yes\"" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Avoid systemctl
# Execute update_birdnet_snippets
export RECS_DIR="$HOME/BirdSongs"
export EXTRACTED="$HOME/BirdSongs/Extracted"
chmod +x "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh
"$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh

View File

@@ -0,0 +1,84 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
set -e
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
############
# SET MQTT #
############
# Function to perform common setup steps
common_steps() {
# Attempt to connect to the MQTT broker
TOPIC="birdnet"
if mosquitto_pub -h "$MQTT_HOST" -p "$MQTT_PORT" -t "$TOPIC" -m "test" -u "$MQTT_USER" -P "$MQTT_PASS" -q 1 -d --will-topic "$TOPIC" --will-payload "Disconnected" --will-qos 1 --will-retain > /dev/null 2>&1; then
# Adapt script with MQTT settings
sed -i "s|%%mqtt_server%%|$MQTT_HOST|g" /helpers/birdnet_to_mqtt.py
sed -i "s|\"%%mqtt_port%%\"|$MQTT_PORT|g" /helpers/birdnet_to_mqtt.py
sed -i "s|%%mqtt_user%%|$MQTT_USER|g" /helpers/birdnet_to_mqtt.py
sed -i "s|%%mqtt_pass%%|$MQTT_PASS|g" /helpers/birdnet_to_mqtt.py
# Copy script to the appropriate directory
cp /helpers/birdnet_to_mqtt.py "$HOME"/BirdNET-Pi/scripts/utils/birdnet_to_mqtt.py
chown pi:pi "$HOME"/BirdNET-Pi/scripts/utils/birdnet_to_mqtt.py
chmod +x "$HOME"/BirdNET-Pi/scripts/utils/birdnet_to_mqtt.py
# Add hooks to the main analysis script
sed -i "/load_global_model, run_analysis/a from utils.birdnet_to_mqtt import automatic_mqtt_publish" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i '/write_to_db(/a\ automatic_mqtt_publish(file, detection, os.path.basename(detection.file_name_extr))' "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
else
bashio::log.fatal "MQTT connection failed, it will not be configured"
fi
}
# Check if MQTT service is available and not disabled
if [[ -f "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py ]] && bashio::services.available 'mqtt' && ! bashio::config.true 'MQTT_DISABLED'; then
bashio::log.green "---"
bashio::log.blue "MQTT addon is active on your system! Birdnet-pi is now automatically configured to send its output to MQTT"
bashio::log.blue "MQTT user : $(bashio::services "mqtt" "username")"
bashio::log.blue "MQTT password : $(bashio::services "mqtt" "password")"
bashio::log.blue "MQTT broker : tcp://$(bashio::services "mqtt" "host"):$(bashio::services "mqtt" "port")"
bashio::log.green "---"
bashio::log.blue "Data will be posted to the topic : 'birdnet'"
bashio::log.blue "Json data : {'Date', 'Time', 'ScientificName', 'CommonName', 'Confidence', 'SpeciesCode', 'ClipName', 'url'}"
bashio::log.blue "---"
# Apply MQTT settings
MQTT_HOST="$(bashio::services "mqtt" "host")"
MQTT_PORT="$(bashio::services "mqtt" "port")"
MQTT_USER="$(bashio::services "mqtt" "username")"
MQTT_PASS="$(bashio::services "mqtt" "password")"
# Perform common setup steps
common_steps
# Check if manual MQTT configuration is provided
elif [[ -f "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py ]] && bashio::config.has_value "MQTT_HOST_manual" && bashio::config.has_value "MQTT_PORT_manual"; then
bashio::log.green "---"
bashio::log.blue "MQTT is manually configured in the addon options"
bashio::log.blue "Birdnet-pi is now automatically configured to send its output to MQTT"
bashio::log.green "---"
bashio::log.blue "Data will be posted to the topic : 'birdnet'"
bashio::log.blue "Json data : {'Date', 'Time', 'ScientificName', 'CommonName', 'Confidence', 'SpeciesCode', 'ClipName', 'url'}"
bashio::log.blue "---"
# Apply manual MQTT settings
MQTT_HOST="$(bashio::config "MQTT_HOST_manual")"
MQTT_PORT="$(bashio::config "MQTT_PORT_manual")"
MQTT_USER="$(bashio::config "MQTT_USER_manual")"
MQTT_PASS="$(bashio::config "MQTT_PASSWORD_manual")"
# Perform common setup steps
common_steps
fi

View File

@@ -0,0 +1,175 @@
#!/command/with-contenv bashio
# shellcheck shell=bash disable=SC2016
set -e
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
################
# MODIFY WEBUI #
################
bashio::log.info "Adapting webui"
# HA specific elements
######################
if bashio::supervisor.ping 2> /dev/null; then
# Remove services tab from webui
echo "... removing System Controls from webui as should be used from HA"
sed -i '/>System Controls/d' "$HOME/BirdNET-Pi/homepage/views.php"
# Remove pulseaudio
echo "... disabling pulseaudio as managed by HomeAssistant"
grep -srl "pulseaudio --start" "$HOME/BirdNET-Pi/scripts" | while read -r file; do
sed -i "s|! pulseaudio --check|pulseaudio --check|g" "$file"
done
# Check if port 80 is correctly configured
if [ -n "$(bashio::addon.port "80")" ] && [ "$(bashio::addon.port "80")" != 80 ]; then
bashio::log.fatal "The port 80 is enabled, but should still be 80 if you want automatic SSL certificates generation to work."
fi
fi
# General elements
##################
# Avoid updates
echo "... modifying the config to silence update indicators"
# Remove if two lines
if [ "$(grep -c '^SILENCE_UPDATE_INDICATOR' /config/birdnet.conf)" -ge 2 ]; then
awk '/^SILENCE_UPDATE_INDICATOR/ { if (++n == 2) next } { print }' /config/birdnet.conf > /config/birdnet.conf.tmp && mv /config/birdnet.conf.tmp /config/birdnet.conf
fi
sed -i "/^SILENCE_UPDATE_INDICATOR/c SILENCE_UPDATE_INDICATOR=1" /config/birdnet.conf
sed -i 's/"\.\$updatediv\.\"//g' "$HOME"/BirdNET-Pi/homepage/views.php
# Correct language labels according to birdnet.conf
echo "... adapting labels according to birdnet.conf"
if grep -q '^DATABASE_LANG=' /config/birdnet.conf; then
export "$(grep -m1 '^DATABASE_LANG=' /config/birdnet.conf)"
bashio::log.info "Setting language to ${DATABASE_LANG:-en}"
else
bashio::log.warning "DATABASE_LANG not found in configuration. Using default labels."
fi
/./"$HOME"/BirdNET-Pi/scripts/install_language_label.sh
# Remove Ram drive option from webui
echo "... removing Ram drive from webui as it is handled from HA"
if grep -q "Ram drive" "$HOME/BirdNET-Pi/scripts/service_controls.php"; then
sed -i '/Ram drive/{n;s/center"/center" style="display: none;"/;}' "$HOME/BirdNET-Pi/scripts/service_controls.php"
sed -i '/Ram drive/d' "$HOME/BirdNET-Pi/scripts/service_controls.php"
fi
# Allow symlinks
echo "... ensuring symlinks work"
for files in "$HOME"/BirdNET-Pi/scripts/*.sh; do
sed -i "s|find |find -L |g" "$files"
sed -i "s|find -L -L |find -L |g" "$files"
done
# Correct services to start as user pi
echo "... updating services to start as user pi"
if ! grep -q "/usr/bin/sudo" "$HOME/BirdNET-Pi/templates/birdnet_analysis.service"; then
while IFS= read -r file; do
if [[ "$(basename "$file")" != "birdnet_log.service" ]]; then
sed -i "s|ExecStart=|ExecStart=/usr/bin/sudo -u pi |g" "$file"
fi
done < <(find "$HOME/BirdNET-Pi/templates/" -name "*net*.service" -print)
fi
# Allow pulseaudio system
echo "... allow pulseaudio as root as backup"
sed -i 's#pulseaudio --start#pulseaudio --start || pulseaudio --system#g' "$HOME"/BirdNET-Pi/scripts/birdnet_recording.sh
# Send services log to container logs
echo "... redirecting services logs to container logs"
while IFS= read -r file; do
sed -i "/StandardError/d" "$file"
sed -i "/StandardOutput/d" "$file"
sed -i "/\[Service/a StandardError=append:/proc/1/fd/1" "$file"
sed -i "/\[Service/a StandardOutput=append:/proc/1/fd/1" "$file"
done < <(find "$HOME/BirdNET-Pi/templates/" -name "*.service" -print)
# Preencode API key
if [[ -f "$HOME/BirdNET-Pi/scripts/common.php" ]] && ! grep -q "221160312" "$HOME/BirdNET-Pi/scripts/common.php"; then
sed -i "/return \$_SESSION\['my_config'\];/i\ \ \ \ if (isset(\$_SESSION\['my_config'\]) \&\& empty(\$_SESSION\['my_config'\]\['FLICKR_API_KEY'\])) {\n\ \ \ \ \ \ \ \ \$_SESSION\['my_config'\]\['FLICKR_API_KEY'\] = \"221160312e1c22\";\n\ \ \ \ }" "$HOME"/BirdNET-Pi/scripts/common.php
sed -i "s|e1c22|e1c22ec60ecf336951b0e77|g" "$HOME"/BirdNET-Pi/scripts/common.php
fi
# Correct log services to show /proc/1/fd/1
echo "... redirecting birdnet_log service output to /logs"
sed -i "/User=pi/d" "$HOME/BirdNET-Pi/templates/birdnet_log.service"
sed -i "s|birdnet_log.sh|cat /proc/1/fd/1|g" "$HOME/BirdNET-Pi/templates/birdnet_log.service"
# Correct backup script
if [[ -f "$HOME/BirdNET-Pi/scripts/backup_data.sh" ]]; then
echo "... correct backup script"
sed -i "/PHP_SERVICE=/c PHP_SERVICE=\$(systemctl list-unit-files -t service --no-pager | grep 'php' | grep 'fpm' | awk '{print \$1}')" "$HOME/BirdNET-Pi/scripts/backup_data.sh"
fi
# Caddyfile modifications
echo "... modifying Caddyfile configurations"
caddy fmt --overwrite /etc/caddy/Caddyfile
#Change port to leave 80 free for certificate requests
if ! grep -q "http://:8081" /etc/caddy/Caddyfile; then
sed -i "s|http://|http://:8081|g" /etc/caddy/Caddyfile
sed -i "s|http://|http://:8081|g" "$HOME/BirdNET-Pi/scripts/update_caddyfile.sh"
if [ -f /etc/caddy/Caddyfile.original ]; then
rm /etc/caddy/Caddyfile.original
fi
fi
# Fix weekly report
sed -i 's|localhost|localhost:8081|g' "$HOME/BirdNET-Pi/scripts/weekly_report.sh"
# Correct webui paths
echo "... correcting webui paths"
if ! grep -q "/stats/" "$HOME/BirdNET-Pi/homepage/views.php"; then
sed -i "s|/stats|/stats/|g" "$HOME/BirdNET-Pi/homepage/views.php"
sed -i "s|/log|/log/|g" "$HOME/BirdNET-Pi/homepage/views.php"
fi
# Correct systemctl path
if [ -f /helpers/systemctl ] && [ -f /helpers/journalctl ]; then
echo "... updating systemctl and journalctl"
cp -rf /helpers/systemctl /bin/systemctl
cp -rf /helpers/journalctl /bin/journalctl
chown pi:pi /bin/systemctl /bin/journalctl
chmod a+x /bin/systemctl /bin/journalctl
fi
# Allow reverse proxy for streamlit
echo "... allow reverse proxy for streamlit"
sed -i "s|plotly_streamlit.py --browser.gatherUsageStats|plotly_streamlit.py --server.enableXsrfProtection=false --server.enableCORS=false --browser.gatherUsageStats|g" "$HOME/BirdNET-Pi/templates/birdnet_stats.service"
# Clean saved mp3 files
if [[ -f "$HOME/BirdNET-Pi/scripts/utils/reporting.py" ]]; then
echo "... add highpass and lowpass to sox extracts"
sed -i "s|f'={stop}']|f'={stop}', 'highpass', '250']|g" "$HOME/BirdNET-Pi/scripts/utils/reporting.py"
fi
# Correct timedatectl path
echo "... updating timedatectl path"
if [[ -f /helpers/timedatectl ]]; then
mv /helpers/timedatectl /usr/bin/timedatectl
chown pi:pi /usr/bin/timedatectl
chmod a+x /usr/bin/timedatectl
fi
# Set RECS_DIR
echo "... setting RECS_DIR to /tmp"
grep -rl "RECS_DIR" "$HOME" --exclude="*.php" | while read -r file; do
sed -i "s|conf\['RECS_DIR'\]|'/tmp'|g" "$file"
sed -i "s|\$RECS_DIR|/tmp|g" "$file"
sed -i "s|\${RECS_DIR}|/tmp|g" "$file"
sed -i "/^RECS_DIR=/c RECS_DIR=/tmp" "$file"
sed -i "/^\$RECS_DIR=/c \$RECS_DIR=/tmp" "$file"
done
mkdir -p /tmp

View File

@@ -0,0 +1,76 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
set -e
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
#################
# NGINX SETTING #
#################
# Variables
ingress_port=$(bashio::addon.ingress_port)
ingress_interface=$(bashio::addon.ip_address)
ingress_entry=$(bashio::addon.ingress_entry)
# Quits if ingress is not active
if [[ "$ingress_entry" != "/api"* ]]; then
bashio::log.info "Ingress entry is not set, exiting configuration."
sed -i "1a sleep infinity" /custom-services.d/02-nginx.sh
exit 0
fi
bashio::log.info "Adapting for ingress"
echo "... setting up nginx"
# Check if the NGINX configuration file exists
nginx_conf="/etc/nginx/servers/ingress.conf"
if [ -f "$nginx_conf" ]; then
sed -i "s/%%port%%/${ingress_port}/g" "$nginx_conf"
sed -i "s/%%interface%%/${ingress_interface}/g" "$nginx_conf"
sed -i "s|%%ingress_entry%%|${ingress_entry}|g" "$nginx_conf"
else
bashio::log.error "NGINX configuration file not found: $nginx_conf"
exit 1
fi
# Disable log
sed -i "/View Log/d" "$HOME/BirdNET-Pi/homepage/views.php"
echo "... ensuring restricted area access"
echo "${ingress_entry}" > /ingress_url
# Modify PHP file safely
php_file="$HOME/BirdNET-Pi/scripts/common.php"
if [ -f "$php_file" ]; then
sed -i "/function is_authenticated/a if (strpos(\$_SERVER['HTTP_REFERER'], '/api/hassio_ingress') !== false && strpos(\$_SERVER['HTTP_REFERER'], trim(file_get_contents('/ingress_url'))) !== false) { \$ret = true; return \$ret; }" "$php_file"
else
bashio::log.warning "PHP file not found: $php_file"
fi
echo "... adapting Caddyfile for ingress"
chmod +x /helpers/caddy_ingress.sh
# Correct script execution
/helpers/caddy_ingress.sh
# Correct API images
sed -i "s|localhost|localhost:8082|g" "$HOME/BirdNET-Pi/scripts/utils/notifications.py"
# Update the Caddyfile if update script exists
caddy_update_script="$HOME/BirdNET-Pi/scripts/update_caddyfile.sh"
if [ -f "$caddy_update_script" ]; then
sed -i "/sudo caddy fmt --overwrite/i /helpers/caddy_ingress.sh" "$caddy_update_script"
else
bashio::log.error "Caddy update script not found: $caddy_update_script"
exit 1
fi

View File

@@ -0,0 +1,49 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
set -e
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
###############
# SSL SETTING #
###############
if bashio::config.true 'ssl'; then
bashio::log.info "SSL is enabled using addon options, setting up NGINX and Caddy."
# Check required SSL configurations
bashio::config.require.ssl
certfile=$(bashio::config 'certfile')
keyfile=$(bashio::config 'keyfile')
# Ensure Caddyfile exists before modifying
caddyfile="/etc/caddy/Caddyfile"
if [ -f "$caddyfile" ]; then
sed -i "2a\ tls /ssl/${certfile} /ssl/${keyfile}" "$caddyfile"
sed -i "s|http://:8081|https://:8081|g" "$caddyfile"
else
bashio::log.error "Caddyfile not found at $caddyfile, skipping SSL configuration."
exit 1
fi
# Ensure update_caddyfile.sh exists before modifying
update_script="$HOME/BirdNET-Pi/scripts/update_caddyfile.sh"
if [ -f "$update_script" ]; then
sed -i "s|http://:8081|https://:8081|g" "$update_script"
if ! grep -q "tls /ssl/${certfile} /ssl/${keyfile}" "$update_script"; then
sed -i "/https:/a\ tls /ssl/${certfile} /ssl/${keyfile}" "$update_script"
fi
else
bashio::log.error "Update script not found: $update_script, skipping SSL setup for update."
exit 1
fi
fi

View File

@@ -0,0 +1,37 @@
#!/command/with-contenv bashio
# shellcheck shell=bash disable=SC1091
set -e
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
######################
# INSTALL TENSORFLOW #
######################
# Check if the CPU supports AVX2
if [[ "$(uname -m)" = "x86_64" ]]; then
if lscpu | grep -q "Flags"; then
if ! lscpu | grep -q "avx2"; then
bashio::log.warning "NON SUPPORTED CPU DETECTED"
bashio::log.warning "Your cpu doesn't support avx2, the analyzer service will likely won't work"
bashio::log.warning "Trying to install tensorflow instead of tflite_runtime instead. This might take some time (up to 5 minutes)."
bashio::log.warning "You could try also Birdnet-Go which should supports your cpu"
source /home/pi/BirdNET-Pi/birdnet/bin/activate
mkdir -p /home/pi/.cache/pip || true &> /dev/null
chmod 755 /home/pi/.cache/pip || true &> /dev/null
pip3 uninstall -y tflite_runtime
pip install --upgrade packaging==23.2
pip3 install --upgrade --force-reinstall "https://github.com/snowzach/tensorflow-multiarch/releases/download/v2.16.1/tensorflow-2.16.1-cp311-cp311-linux_x86_64.whl"
deactivate
fi
fi
fi

View File

@@ -0,0 +1,107 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
set -eu
##################
# ALLOW RESTARTS #
##################
if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
mkdir -p /etc/scripts-init
sed -i "s|/etc/cont-init.d|/etc/scripts-init|g" /ha_entrypoint.sh
sed -i "/ rm/d" /ha_entrypoint.sh
cp "${BASH_SOURCE[0]}" /etc/scripts-init/
fi
##############
# SET SYSTEM #
##############
# Set password
bashio::log.info "Setting password for the user pi"
if bashio::config.has_value "pi_password"; then
PI_PASSWORD="$(bashio::config "pi_password")"
elif [[ -n "${pi_password:-}" ]]; then
PI_PASSWORD="${pi_password}"
fi
if [[ -n "${PI_PASSWORD:-}" ]]; then
echo "pi:${PI_PASSWORD}" | chpasswd
bashio::log.info "Password set successfully for user pi."
else
# Set empty password to allow web terminal login when no password is configured
passwd -d pi
bashio::log.info "No password specified for user pi. Enabled passwordless login."
fi
# Use timezone defined in add-on options if available
bashio::log.info "Setting timezone :"
if bashio::config.has_value 'TZ'; then
TZ_VALUE="$(bashio::config 'TZ')"
if timedatectl set-timezone "$TZ_VALUE"; then
echo "... timezone set to $TZ_VALUE as defined in add-on options (BirdNET config ignored)."
else
bashio::log.warning "Couldn't set timezone to $TZ_VALUE. Refer to the list of valid timezones: https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html"
timedatectl set-ntp true &> /dev/null
fi
# Use BirdNET-defined timezone if no add-on option is provided
elif [ -f /data/timezone ]; then
BIRDN_CONFIG_TZ="$(cat /data/timezone)"
timedatectl set-ntp false &> /dev/null
if timedatectl set-timezone "$BIRDN_CONFIG_TZ"; then
echo "... set to $BIRDN_CONFIG_TZ as defined in BirdNET config."
else
bashio::log.warning "Couldn't set timezone to $BIRDN_CONFIG_TZ. Reverting to automatic timezone."
timedatectl set-ntp true &> /dev/null
fi
# Fallback to automatic timezone if no manual settings are found
else
if timedatectl set-ntp true &> /dev/null; then
bashio::log.info "... automatic timezone enabled."
else
bashio::log.fatal "Couldn't set automatic timezone! Please set a manual one from the options."
fi
fi || true
# Use ALSA CARD defined in add-on options if available
if [ -n "${ALSA_CARD:-}" ]; then
bashio::log.warning "ALSA_CARD is defined, the birdnet.conf is adapt to use device $ALSA_CARD"
for file in "$HOME"/BirdNET-Pi/birdnet.conf /config/birdnet.conf; do
if [ -f "$file" ]; then
sed -i "/^REC_CARD/c\REC_CARD=$ALSA_CARD" "$file"
fi
done
fi
# Define permissions for audio
AUDIO_GID=$(stat -c %g /dev/snd/* | head -n1) \
&& (groupmod -o -g "$AUDIO_GID" audio 2> /dev/null || groupadd -o -g "$AUDIO_GID" audio || true) \
&& usermod -aG audio "${USER:-pi}" || true
# Fix timezone as per installer
CURRENT_TIMEZONE="$(timedatectl show --value --property=Timezone)"
[ -f /etc/timezone ] && echo "$CURRENT_TIMEZONE" | sudo tee /etc/timezone > /dev/null
bashio::log.info "Starting system services"
bashio::log.info "Starting cron service"
systemctl start cron > /dev/null
bashio::log.info "Starting dbus service"
service dbus start > /dev/null
bashio::log.info "Starting BirdNET-Pi services"
chmod +x "$HOME/BirdNET-Pi/scripts/restart_services.sh" > /dev/null
"$HOME/BirdNET-Pi/scripts/restart_services.sh" > /dev/null
# Start livestream services if enabled in configuration
if bashio::config.true "LIVESTREAM_BOOT_ENABLED"; then
echo "... starting livestream services"
systemctl enable icecast2 > /dev/null
systemctl start icecast2.service > /dev/null
systemctl enable --now livestream.service > /dev/null
fi
# Start
bashio::log.info "✅ Setup complete."

View File

@@ -0,0 +1,96 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -0,0 +1,16 @@
proxy_http_version 1.1;
proxy_ignore_client_abort off;
proxy_read_timeout 86400s;
proxy_redirect off;
proxy_send_timeout 86400s;
proxy_max_temp_file_size 0;
proxy_hide_header X-Frame-Options;
proxy_set_header Accept-Encoding "";
proxy_set_header Connection $connection_upgrade;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -0,0 +1 @@
resolver 127.0.0.11 ipv6=off;

View File

@@ -0,0 +1,6 @@
root /dev/null;
server_name $hostname;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;

View File

@@ -0,0 +1,9 @@
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

View File

@@ -0,0 +1,3 @@
upstream backend {
server 127.0.0.1:80;
}

View File

@@ -0,0 +1,78 @@
# Run nginx in foreground.
daemon off;
# This is run inside Docker.
user root;
# Pid storage location.
pid /var/run/nginx.pid;
# Set number of worker processes.
worker_processes auto;
# Enables the use of JIT for regular expressions to speed-up their processing.
pcre_jit on;
# Write error log to Hass.io add-on log.
error_log /proc/1/fd/1 error;
# Load allowed environment vars
env HASSIO_TOKEN;
# Load dynamic modules.
include /etc/nginx/modules/*.conf;
# Max num of simultaneous connections by a worker process.
events {
worker_connections 8192;
}
http {
include /etc/nginx/includes/mime.types;
# https://emby.media/community/index.php?/topic/93074-how-to-emby-with-nginx-with-windows-specific-tips-and-csp-options/
server_names_hash_bucket_size 64;
gzip_disable "msie6";
gzip_comp_level 6;
gzip_min_length 1100;
gzip_buffers 16 8k;
gzip_proxied any;
gzip_types
text/plain
text/css
text/js
text/xml
text/javascript
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
image/svg+xml;
proxy_connect_timeout 1h;
log_format hassio '[$time_local] $status '
'$http_x_forwarded_for($remote_addr) '
'$request ($http_user_agent)';
access_log /proc/1/fd/1 hassio;
client_max_body_size 4G;
default_type application/octet-stream;
gzip on;
keepalive_timeout 65;
sendfile on;
server_tokens off;
tcp_nodelay on;
tcp_nopush on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
include /etc/nginx/includes/resolver.conf;
include /etc/nginx/includes/upstream.conf;
include /etc/nginx/servers/*.conf;
}

View File

@@ -0,0 +1,41 @@
server {
listen %%interface%%:%%port%% default_server;
include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
proxy_buffering off;
auth_basic_user_file /home/pi/.htpasswd;
location / {
# Proxy pass
proxy_pass http://localhost:8082;
# Next three lines allow websockets
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Adjust any Location headers in backend redirects
absolute_redirect off;
proxy_redirect /stats %%ingress_entry%%/stats;
proxy_redirect /log %%ingress_entry%%/log;
proxy_redirect /terminal %%ingress_entry%%/terminal;
# Correct base_url
proxy_set_header Accept-Encoding "";
sub_filter_once off;
sub_filter_types *;
sub_filter /spectrogram %%ingress_entry%%/spectrogram;
sub_filter /By_Date/ %%ingress_entry%%/By_Date/;
sub_filter /Charts/ %%ingress_entry%%/Charts/;
sub_filter /stats/ %%ingress_entry%%/stats/;
sub_filter /log/ %%ingress_entry%%/log/;
sub_filter /terminal/ %%ingress_entry%%/terminal/;
sub_filter "url = '/" "url = '%%ingress_entry%%/";
sub_filter /todays %%ingress_entry%%/todays;
sub_filter href=\"/ href=\"%%ingress_entry%%/;
sub_filter src=\"/ src=\"%%ingress_entry%%/;
sub_filter hx-get=\"/ hx-get=\"%%ingress_entry%%/;
sub_filter action=\"/ action=\"%%ingress_entry%%/;
}
}

View File

@@ -0,0 +1,141 @@
#! /usr/bin/env python3
# birdnet_to_mqtt.py
import json
import logging
import os
import re
import sys
import paho.mqtt.client as mqtt
import requests
utils_path = os.path.expanduser("~/BirdNET-Pi/scripts/utils")
sys.path.append(utils_path)
from helpers import get_settings
# Setup basic configuration for logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
# Used in flickrimage
flickr_images = {}
conf = get_settings()
settings_dict = dict(conf)
# MQTT server configuration
mqtt_server = "%%mqtt_server%%"
mqtt_user = "%%mqtt_user%%"
mqtt_pass = "%%mqtt_pass%%"
mqtt_port = "%%mqtt_port%%"
mqtt_topic = "birdnet"
bird_lookup_url_base = "http://en.wikipedia.org/wiki/"
def on_connect(client, userdata, flags, rc): # , properties=None):
"""Callback for when the client receives a CONNACK response from the server."""
if rc == 0:
log.info("Connected to MQTT Broker!")
else:
log.error(f"Failed to connect, return code {rc}\n")
def get_bird_code(scientific_name):
with open("/home/pi/BirdNET-Pi/scripts/ebird.php", "r") as file:
data = file.read()
array_str = re.search(r"\$ebirds = \[(.*?)\];", data, re.DOTALL).group(1)
bird_dict = {
re.search(r'"(.*?)"', line).group(1): re.search(r'=> "(.*?)"', line).group(1)
for line in array_str.split("\n")
if "=>" in line
}
return bird_dict.get(scientific_name)
def automatic_mqtt_publish(file, detection, path):
bird = {}
bird["Date"] = detection.date
bird["Time"] = detection.time
bird["ScientificName"] = detection.scientific_name.replace("_", " ")
bird["CommonName"] = detection.common_name
bird["Confidence"] = detection.confidence
bird["SpeciesCode"] = get_bird_code(detection.scientific_name)
bird["ClipName"] = path
bird["url"] = bird_lookup_url_base + detection.scientific_name.replace(" ", "_")
# Flickimage
image_url = ""
common_name = detection.common_name
if len(settings_dict.get("FLICKR_API_KEY")) > 0:
if common_name not in flickr_images:
try:
headers = {"User-Agent": "Python_Flickr/1.0"}
url = (
"https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key="
+ str(settings_dict.get("FLICKR_API_KEY"))
+ "&text="
+ str(common_name)
+ " bird&sort=relevance&per_page=5&media=photos&format=json&license=2%2C3%2C4%2C5%2C6%2C9"
+ "&nojsoncallback=1"
)
resp = requests.get(url=url, headers=headers, timeout=10)
resp.encoding = "utf-8"
data = resp.json()["photos"]["photo"][0]
image_url = (
"https://farm"
+ str(data["farm"])
+ ".static.flickr.com/"
+ str(data["server"])
+ "/"
+ str(data["id"])
+ "_"
+ str(data["secret"])
+ "_n.jpg"
)
flickr_images[common_name] = image_url
except Exception as e:
print("FLICKR API ERROR: " + str(e))
image_url = ""
else:
image_url = flickr_images[common_name]
bird["FlickrImage"] = image_url
json_bird = json.dumps(bird)
mqttc.reconnect()
mqttc.publish(mqtt_topic, json_bird, 1)
log.info("Posted to MQTT: ok")
# Create MQTT client using legacy callback API when available for
# compatibility with paho-mqtt >= 2.0
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
if callback_api is not None:
# paho-mqtt >= 2.0: first argument is callback_api_version
mqttc = mqtt.Client(callback_api.VERSION1, client_id="birdnet_mqtt")
else:
# paho-mqtt < 2.0: old signature, first argument is client_id
mqttc = mqtt.Client(client_id="birdnet_mqtt")
mqttc.username_pw_set(mqtt_user, mqtt_pass)
mqttc.on_connect = on_connect
try:
mqttc.connect(mqtt_server, mqtt_port)
mqttc.loop_start()
# Assuming `file` and `detections` are provided from somewhere
# automatic_mqtt_publish(file, detections)
except Exception as e:
log.error("Cannot post mqtt: %s", e)
finally:
mqttc.loop_stop()
mqttc.disconnect()

View File

@@ -0,0 +1,26 @@
#!/bin/bash
# shellcheck shell=bash
# Get values
set +u
# shellcheck disable=SC1091
source /etc/birdnet/birdnet.conf
# Create ingress configuration for Caddyfile
cat << EOF >> /etc/caddy/Caddyfile
:8082 {
root * ${EXTRACTED}
file_server browse
handle /By_Date/* {
file_server browse
}
handle /Charts/* {
file_server browse
}
reverse_proxy /stream localhost:8000
php_fastcgi unix//run/php/php-fpm.sock
reverse_proxy /log* localhost:8080
reverse_proxy /stats* localhost:8501
reverse_proxy /terminal* localhost:8888
}
EOF

View File

@@ -0,0 +1,107 @@
#!/bin/bash
# Function to show the current timezone using two alternative methods
show_timezone() {
if [ -f /data/timezone ]; then
cat /data/timezone
elif [ -f /etc/timezone ]; then
cat /etc/timezone
elif [ -f /etc/localtime ]; then
readlink /etc/localtime | sed 's|/usr/share/zoneinfo/||'
else
echo "Cannot determine timezone."
fi
}
# Function to set the timezone
set_timezone() {
local new_timezone="$1"
if [ ! -f "/usr/share/zoneinfo/$new_timezone" ]; then
echo "Invalid timezone: $new_timezone"
return 1
fi
echo "$new_timezone" >/data/timezone
echo "$new_timezone" >/etc/timezone
ln -sf "/usr/share/zoneinfo/$new_timezone" /etc/localtime
# Update /etc/environment if it exists
if [ -f /etc/environment ]; then
sed -i "/^TZ=/c\TZ=$new_timezone" /etc/environment
fi
# Update s6 container environment if it exists
if [ -d /var/run/s6/container_environment ]; then
echo "$new_timezone" >/var/run/s6/container_environment/TZ
fi
echo "Timezone set to: $new_timezone"
}
# Function to enable or disable NTP
set_ntp() {
case "$1" in
"false")
systemctl stop systemd-timesyncd
systemctl disable systemd-timesyncd
echo "NTP disabled"
;;
"true")
systemctl start systemd-timesyncd
systemctl enable systemd-timesyncd
# Remove the /data/timezone file when NTP is enabled
if [ -f /data/timezone ]; then
rm -f /data/timezone
echo "Timezone configuration file /data/timezone deleted."
fi
echo "NTP enabled"
;;
*)
echo "Invalid argument for set-ntp. Use 'false' or 'true'."
;;
esac
}
# Function to show detailed time settings
show_time_details() {
local local_time
local utc_time
local time_zone
local ntp_status="no"
local ntp_service="inactive"
local_time="$(date)"
utc_time="$(date -u)"
time_zone="$(show_timezone)"
# Check if NTP is used
if systemctl is-active --quiet systemd-timesyncd; then
ntp_status="yes"
ntp_service="active"
fi
# Print the information
echo "Local time: $local_time"
echo "Universal time: $utc_time"
echo "Time zone: $time_zone"
echo "Network time on: $ntp_status"
echo "NTP service: $ntp_service"
}
# Main script logic
case "$1" in
"set-ntp")
set_ntp "$2"
;;
"show")
show_timezone
;;
"set-timezone")
set_timezone "$2"
;;
*)
show_time_details
;;
esac

View File

@@ -0,0 +1,268 @@
#!/usr/bin/env bash
#
# A simple bash script to replicate essential "timedatectl" functionality:
# - Show current time and time zone
# - List available time zones
# - Set time or time zone
# - Enable/disable/verify NTP
# - Configure RTC as localtime or UTC
# ------------------------------------------------------------------------------
# Utility functions
# ------------------------------------------------------------------------------
print_usage() {
cat <<EOF
Usage: $(basename "$0") COMMAND [ARGS...]
Commands:
status Show current time/date, timezone, RTC mode, NTP status
set-time [TIME] Set system clock to TIME (e.g. "2025-03-21 18:00:00")
set-timezone [ZONE] Set system timezone to ZONE (Region/City)
list-timezones List all known timezones in /usr/share/zoneinfo
set-local-rtc [0|1] Set whether RTC is in local time (1) or UTC (0)
set-ntp [true|false] Enable or disable systemd-timesyncd (if available)
Examples:
$(basename "$0") status
$(basename "$0") set-time "2025-03-21 18:00:00"
$(basename "$0") set-timezone America/New_York
$(basename "$0") set-local-rtc 1
$(basename "$0") set-ntp true
Note: Most operations require root privileges. Use sudo if necessary.
EOF
}
# Safely exit on errors
abort() {
echo "Error: $*" >&2
exit 1
}
# ------------------------------------------------------------------------------
# Functions for each major operation
# ------------------------------------------------------------------------------
show_status() {
echo "=== System Time/Date ==="
date
echo
echo "=== Time Zone ==="
# Attempt to parse the symlink at /etc/localtime
# On many systems, /etc/localtime is a symlink to /usr/share/zoneinfo/Region/City
if [ -L /etc/localtime ]; then
local tz_target
tz_target=$(readlink -f /etc/localtime)
# Remove the /usr/share/zoneinfo/ part to display just Region/City
local tz_name
tz_name="${tz_target#/usr/share/zoneinfo/}"
echo "Time zone: $tz_name"
else
# Some distros have /etc/localtime as a copy of the zone file
# Try to read /etc/timezone as a fallback (Debian-based)
if [ -f /etc/timezone ]; then
echo "Time zone: $(cat /etc/timezone)"
else
echo "Time zone: Unknown (not a symlink, and /etc/timezone missing)"
fi
fi
echo
echo "=== RTC in local TZ? ==="
# If /etc/adjtime exists and the last line is "LOCAL", that typically indicates localtime
# If its "UTC" or absent, the RTC is typically in UTC.
if [ -f /etc/adjtime ]; then
local rtc_mode
rtc_mode=$(tail -n 1 /etc/adjtime)
if [ "$rtc_mode" = "LOCAL" ]; then
echo "RTC is in local time."
else
echo "RTC is in UTC."
fi
else
echo "Cannot determine RTC mode (no /etc/adjtime)."
fi
echo
echo "=== NTP Service Status ==="
# Attempt to detect if systemd-timesyncd is enabled/active
if command -v systemctl &>/dev/null; then
if systemctl is-enabled systemd-timesyncd &>/dev/null; then
echo "systemd-timesyncd service is enabled."
else
echo "systemd-timesyncd service is disabled."
fi
if systemctl is-active systemd-timesyncd &>/dev/null; then
echo "systemd-timesyncd service is active (running)."
else
echo "systemd-timesyncd service is not running."
fi
else
echo "systemctl is not available. Cannot determine NTP status this way."
fi
echo
}
set_time() {
local new_time="$1"
if [ -z "$new_time" ]; then
abort "Please specify a time, e.g. '2025-03-21 18:00:00'"
fi
echo "Setting system time to: $new_time"
# Using 'date' to set system time.
# Format can be e.g. "YYYY-MM-DD HH:MM:SS"
# Usually you need root privileges for this:
if ! date -s "$new_time"; then
abort "Failed to set system time. Check your permissions?"
fi
# Also sync to hardware clock:
if command -v hwclock &>/dev/null; then
hwclock --systohc || echo "Warning: couldn't sync with HW clock."
fi
}
list_timezones() {
local zoneinfo_dir="/usr/share/zoneinfo"
if [ ! -d "$zoneinfo_dir" ]; then
abort "Cannot find $zoneinfo_dir directory."
fi
echo "List of available time zones (under $zoneinfo_dir):"
# We filter out possible files that are not real zone data.
# On some systems, zone data might be in subdirectories (Region/City).
find "$zoneinfo_dir" -type f | sed "s|^$zoneinfo_dir/||"
}
set_timezone() {
local tz="$1"
if [ -z "$tz" ]; then
abort "Please specify a time zone, e.g. 'America/New_York'"
fi
local zoneinfo_file="/usr/share/zoneinfo/$tz"
if [ ! -f "$zoneinfo_file" ]; then
abort "Time zone '$tz' not found under /usr/share/zoneinfo"
fi
echo "Linking /etc/localtime to $zoneinfo_file"
ln -sf "$zoneinfo_file" /etc/localtime || abort "Failed to link /etc/localtime"
# Some distros use /etc/timezone to store the name of the current timezone
if [ -w /etc/timezone ]; then
echo "$tz" >/etc/timezone
fi
echo "Time zone changed to $tz."
}
set_local_rtc() {
local is_local="$1"
if [ -z "$is_local" ]; then
abort "Please specify 0 or 1. Example: set-local-rtc 1"
fi
# We update /etc/adjtime accordingly:
# - If local time: last line should be LOCAL
# - If UTC: last line should be UTC
# We also update the hardware clock accordingly.
if [ "$is_local" = "1" ]; then
echo "Configuring RTC to use local time."
# "hwclock --localtime" is fairly ambiguous (it sets hardware clock from system time).
# We use the below approach:
hwclock --systohc --localtime || echo "Warning: couldn't set HW clock localtime."
# Overwrite /etc/adjtime fully:
cat <<EOF >/etc/adjtime
0.0 0 0.0
0
LOCAL
EOF
else
echo "Configuring RTC to use UTC."
hwclock --systohc --utc || echo "Warning: couldn't set HW clock to UTC."
cat <<EOF >/etc/adjtime
0.0 0 0.0
0
UTC
EOF
fi
}
set_ntp() {
local enable_ntp="$1"
if ! command -v systemctl &>/dev/null; then
echo "systemctl not available. Cannot manage systemd-timesyncd. Exiting."
return 1
fi
# systemd-timesyncd is the typical built-in NTP client for systemd-based systems
case "$enable_ntp" in
true | True | 1 | on)
echo "Enabling and starting systemd-timesyncd..."
systemctl enable systemd-timesyncd || echo "Failed to enable timesyncd."
systemctl start systemd-timesyncd || echo "Failed to start timesyncd."
;;
false | False | 0 | off)
echo "Disabling and stopping systemd-timesyncd..."
systemctl disable systemd-timesyncd || echo "Failed to disable timesyncd."
systemctl stop systemd-timesyncd || echo "Failed to stop timesyncd."
;;
*)
abort "Unknown argument '$enable_ntp'. Use 'true' or 'false'."
;;
esac
}
# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------
main() {
local cmd="$1"
shift || true
case "$cmd" in
status)
show_status
;;
set-time)
set_time "$@"
;;
set-timezone)
set_timezone "$@"
;;
list-timezones)
list_timezones
;;
set-local-rtc)
set_local_rtc "$@"
;;
set-ntp)
set_ntp "$@"
;;
"" | help | --help | -h)
print_usage
;;
*)
echo "Unknown command: $cmd"
print_usage
exit 1
;;
esac
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
if [ $# -lt 1 ]; then
print_usage
exit 0
fi
main "$@"
fi

BIN
birdnet-pi-zach/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,9 @@
{
"last_update": "10-07-2026",
"paused": true,
"repository": "alexbelgium/hassio-addons",
"slug": "birdnet-pi-zach",
"source": "github",
"upstream_repo": "zach7036/BirdNET-Pi-Enhanced-Version",
"upstream_version": ""
}