refactor(birdnet-pi-zach): share the wrapper layer with birdnet-pi through symlinks (#3154) (#3175)

birdnet-pi-zach kept real copies of birdnet-pi's custom-services.d, etc and
helpers, so birdnet-pi fixes silently skipped it. Replace the three
directories with symlinks to ../../birdnet-pi/rootfs/..., exactly as
battybirdnet-pi does; CI materialises them with resolve_symlinks.sh before
the build.

16 files were already byte-identical. The other 6 had only lagged behind
birdnet-pi fixes zach never received, none is zach-specific:
- 02-caddy.sh, 00-php_pfm.sh: no hard wait on D-Bus, /run/php created,
  TZ fallback, ingress site re-added if update_caddyfile.sh dropped it.
- 91-nginx_ingress.sh, caddy_ingress.sh: the ingress hook is idempotent
  (no duplicate :8082 site), accepts the anchor with or without sudo, and
  is verified after insertion; /ingress_url is cleared when ingress is off.
- 01-avahi.sh, 02-nginx.sh: shebang bashio -> bash only.
helpers/ now also ships merge_open_prs.sh, which zach's Dockerfile never
calls (battybirdnet-pi ships it unused the same way).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AlexandreandClaude Opus 5.5 authored and GitHub committed 2026-10-28 17:56:21 +01:00
1 parent a8eb64967b
commit cacfe2a4ac
31 files changed
+6 -2046

No files matched your search

+3
View File
@@ -2,6 +2,9 @@
- Fix: restarting the services no longer leaves a previous `daily_plot.py` or recording loop running next to the new one. `restart_services.sh` now clears leftover `daily_plot.py`, `birdnet_recording.sh`, and `ffmpeg`/`arecord` recorders writing to `StreamData` once the recorder is stopped (port of the birdnet-pi fix for #3152)
- Fix: an unreachable audio source (RTSP host switched off, missing soundcard) no longer causes a restart loop with growing RAM. The monitoring service now reports the silent source instead of restarting when there is nothing queued to analyze, and limits the static `analyzing_now.txt` restart path to once every 30 minutes
- Fix: the recording retry loop no longer restarts `ffmpeg` once per second while the audio source is unreachable (100%+ CPU). The retry delay now grows from 1s to 64s, and resets once `ffmpeg` has run for more than two minutes
- The add-on's wrapper scripts (`custom-services.d`, `etc`, `helpers`) are now shared with birdnet-pi through symlinks, as battybirdnet-pi already does, so future birdnet-pi fixes reach this add-on too (#3154)
- Fix: the web UI (PHP-FPM, caddy) no longer waits for D-Bus to start, and creates `/run/php` itself; the timezone falls back to `TZ` when D-Bus is unavailable
- Fix: the ingress site is added to the Caddyfile only once (a duplicate `:8082` site makes caddy refuse to start), and `02-caddy.sh` re-adds it if `update_caddyfile.sh` dropped it
## 2026.10.28 (2026-10-28)
- Update to latest version from zach7036/BirdNET-Pi-Enhanced-Version (changelog : https://github.com/zach7036/BirdNET-Pi-Enhanced-Version/releases)
+1
View File
@@ -0,0 +1 @@
../../birdnet-pi/rootfs/custom-services.d
@@ -1,15 +0,0 @@
#!/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
@@ -1,13 +0,0 @@
#!/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
@@ -1,25 +0,0 @@
#!/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
@@ -1,10 +0,0 @@
#!/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
@@ -1,310 +0,0 @@
#!/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
# Minimum delay between two automatic service restarts (seconds). Restarting
# every couple of minutes has never recovered a stuck analyzer, and each restart
# leaves processes of the previous run behind.
RESTART_MIN_INTERVAL=1800
last_restart_time=0
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
# birdnet_analysis only rewrites 'analyzing_now' when it picks up a new
# recording, so the file also stops changing when there is nothing to
# analyze at all - an unreachable RTSP source, a silent or missing
# soundcard. With an empty queue the analyzer is idle, not stuck, and
# restarting the services cannot bring the audio source back: it only
# leaves a second daily_plot.py and a second recording loop behind every
# couple of minutes. Report it, but do not restart.
local wav_waiting
wav_waiting=$(find -L "$INGEST_DIR" -maxdepth 1 -name '*.wav' | wc -l)
if ((wav_waiting == 0)); then
log_red "$(date) INFO: no recordings to analyze - the audio source looks silent or unreachable. Not restarting."
apprisealert "No new recordings: the audio source looks silent or unreachable"
return 1
fi
local now
now=$(date +%s)
if ((now - last_restart_time < RESTART_MIN_INTERVAL)); then
log_yellow "$(date) INFO: 'analyzing_now' still unchanged, but services were already restarted $(((now - last_restart_time) / 60)) minutes ago. Waiting."
return 1
fi
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"
last_restart_time=$now
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
+1
View File
@@ -0,0 +1 @@
../../birdnet-pi/rootfs/etc
@@ -1,51 +0,0 @@
#!/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
@@ -1,126 +0,0 @@
#!/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
@@ -1,50 +0,0 @@
#!/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
@@ -1,75 +0,0 @@
#!/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::app.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
@@ -1,84 +0,0 @@
#!/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
@@ -1,212 +0,0 @@
#!/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::app.port "80")" ] && [ "$(bashio::app.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
# Back off the recording retry loop
# loop_ffmpeg() retries a failed ffmpeg after a fixed "sleep 1". When the audio
# source is unreachable ffmpeg exits immediately ("No route to host"), so that is
# one ffmpeg start per second for as long as the source is down - enough to keep
# a 2-core host at 100% CPU. Grow the delay 1s -> 64s instead, and reset it once
# ffmpeg has managed to run for more than 2 minutes, so a healthy stream that
# hiccups is still picked up straight away. The sleep only runs when ffmpeg
# fails, so a working source is unaffected.
RECORDING_SCRIPT="$HOME/BirdNET-Pi/scripts/birdnet_recording.sh"
if grep -q '^[[:space:]]*sleep 1$' "$RECORDING_SCRIPT"; then
echo "... backing off the recording retry loop"
sed 's|^\([[:space:]]*\)sleep 1$|\1RETRY_DELAY=$(( SECONDS - ${RETRY_LAST:-0} > 120 ? 1 : ${RETRY_DELAY:-1} ))\n\1sleep "$RETRY_DELAY"\n\1RETRY_LAST=$SECONDS\n\1RETRY_DELAY=$(( RETRY_DELAY < 64 ? RETRY_DELAY * 2 : 64 ))|' \
"$RECORDING_SCRIPT" > "$RECORDING_SCRIPT.backoff"
# Only keep the patched script if it still parses, a broken recording script
# would take down recording altogether
if bash -n "$RECORDING_SCRIPT.backoff"; then
chown --reference="$RECORDING_SCRIPT" "$RECORDING_SCRIPT.backoff"
chmod --reference="$RECORDING_SCRIPT" "$RECORDING_SCRIPT.backoff"
mv "$RECORDING_SCRIPT.backoff" "$RECORDING_SCRIPT"
else
rm -f "$RECORDING_SCRIPT.backoff"
bashio::log.warning "Could not add a backoff to the recording retry loop, leaving it unchanged"
fi
fi
# Clear leftovers of the previous run when restarting the services
# The systemctl replacement has no cgroups: it only stops the processes it can
# still trace back to a living main process. If that one died first, its
# children (loop_ffmpeg shells, ffmpeg, arecord) survive, and every restart adds
# a second daily_plot.py or recording loop next to them. The [x] brackets keep
# the pattern from matching the sudo/pkill command line itself.
RESTART_SCRIPT="$HOME/BirdNET-Pi/scripts/restart_services.sh"
if ! grep -qF '[d]aily_plot' "$RESTART_SCRIPT"; then
echo "... clearing leftover processes on service restarts"
sed -i '/systemctl stop birdnet_recording.service/a sudo pkill -f "[d]aily_plot.py --daemon|/usr/local/bin/[b]irdnet_recording.sh|[f]fmpeg .*StreamData/.*wav|[a]record .*StreamData/.*wav" || true' "$RESTART_SCRIPT"
fi
# 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
@@ -1,76 +0,0 @@
#!/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::app.ingress_port)
ingress_interface=$(bashio::app.ip_address)
ingress_entry=$(bashio::app.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
@@ -1,49 +0,0 @@
#!/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
@@ -1,37 +0,0 @@
#!/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
@@ -1,121 +0,0 @@
#!/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
# REC_CARD is passed as-is to "arecord -D" (scripts/birdnet_recording.sh) and to
# "ffmpeg -f alsa -i" (scripts/livestream.sh), so it must be an ALSA PCM name.
# ALSA_CARD holds a card index (1) or a card id (Audio), which are not PCM names:
# writing them as-is gives "Unknown PCM 1" and no recording at all. Build a PCM
# name from them, unless the value already is one of ALSA's own PCM names
# (checked against "arecord -L", e.g. default, null, pulse, sysdefault, front...).
if [[ "$ALSA_CARD" == *:* ]] || arecord -L 2> /dev/null | grep -qx "$ALSA_CARD"; then
REC_CARD="$ALSA_CARD"
else
REC_CARD="plughw:CARD=${ALSA_CARD},DEV=0"
fi
bashio::log.warning "ALSA_CARD is defined, the birdnet.conf is adapted to use device $REC_CARD"
# --follow-symlinks : $HOME/BirdNET-Pi/birdnet.conf is a symlink to /config/birdnet.conf
# (01-structure.sh), and sed -i would replace it with a regular file, detaching it from
# the file the WebUI writes to. Only /config/birdnet.conf is updated directly, since the
# home-path symlink is writable by the pi/caddy user and could be repointed before this
# root-run script gets to it.
if [ -f /config/birdnet.conf ]; then
sed -i --follow-symlinks "/^REC_CARD/c\REC_CARD=$REC_CARD" /config/birdnet.conf
fi
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."
@@ -1,96 +0,0 @@
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;
}
@@ -1,16 +0,0 @@
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;
@@ -1 +0,0 @@
resolver 127.0.0.11 ipv6=off;
@@ -1,6 +0,0 @@
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;
@@ -1,9 +0,0 @@
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;
@@ -1,3 +0,0 @@
upstream backend {
server 127.0.0.1:80;
}
@@ -1,78 +0,0 @@
# 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;
}
@@ -1,41 +0,0 @@
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%%/;
}
}
+1
View File
@@ -0,0 +1 @@
../../birdnet-pi/rootfs/helpers
@@ -1,141 +0,0 @@
#! /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()
@@ -1,26 +0,0 @@
#!/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
-107
View File
@@ -1,107 +0,0 @@
#!/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
-268
View File
@@ -1,268 +0,0 @@
#!/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 it’s "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