mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-10-30 01:31:43 +00:00
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:
1 parent
a8eb64967b
commit
cacfe2a4ac
31 files changed
+6
-2046
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Symlink
+1
@@ -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%%/;
|
||||
}
|
||||
}
|
||||
Symlink
+1
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user