use symlinks

This commit is contained in:
Alexandre
2025-03-21 07:39:30 +01:00
parent c941a85210
commit 57798c92b5
19 changed files with 850 additions and 465 deletions

View File

@@ -1,105 +1,261 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# shellcheck shell=bash # shellcheck shell=bash
# Adapted from https://github.com/mcguirepr89/BirdNET-Pi/issues/393#issuecomment-1166445710 # Improved BirdNET-Pi Monitoring Script with Recovery Alerts and Condensed Logs
HOME="/home/pi" HOME="/home/pi"
# Define logging functions ########################################
log_green() { echo -e "\033[32m$1\033[0m"; } # Logging Functions (color-coded for terminal clarity)
log_red() { echo -e "\033[31m$1\033[0m"; } ########################################
log_yellow() { echo -e "\033[33m$1\033[0m"; } log_green() { echo -e "\033[32m$1\033[0m"; }
log_info() { echo -e "\033[34m$1\033[0m"; } log_red() { echo -e "\033[31m$1\033[0m"; }
log_yellow() { echo -e "\033[33m$1\033[0m"; }
echo "$(log_green "Starting service: throttlerecording")" log_blue() { echo -e "\033[34m$1\033[0m"; }
touch "$HOME/BirdSongs/StreamData/analyzing_now.txt"
########################################
# Read configuration # Read configuration
########################################
set +u set +u
# shellcheck disable=SC1091
source /etc/birdnet/birdnet.conf source /etc/birdnet/birdnet.conf
# Set constants ########################################
srv="birdnet_recording" # Wait 5 minutes for system stabilization
srv2="birdnet_analysis" ########################################
ingest_dir="$RECS_DIR/StreamData" sleep 5m
counter=10
# Ensure directories and permissions
mkdir -p "$ingest_dir"
chown -R pi:pi "$ingest_dir"
chmod -R 755 "$ingest_dir"
# Function to send notifications using Apprise log_green "Starting service: throttlerecording"
########################################
# Define Directories, Files, and Constants
########################################
INGEST_DIR="$(readlink -f "$HOME/BirdSongs/StreamData")"
ANALYZING_NOW_FILE="$INGEST_DIR/analyzing_now.txt"
touch "$ANALYZING_NOW_FILE"
BIRDSONGS_DIR="$(readlink -f "$HOME/BirdSongs/Extracted/By_Date")"
# Ensure directories and set permissions
mkdir -p "$INGEST_DIR" || { log_red "Failed to create directory: $INGEST_DIR"; exit 1; }
chown -R pi:pi "$INGEST_DIR" || log_yellow "Could not change ownership for $INGEST_DIR"
chmod -R 755 "$INGEST_DIR" || log_yellow "Could not set permissions for $INGEST_DIR"
# Services to monitor
SERVICES=(birdnet_analysis chart_viewer spectrogram_viewer birdnet_recording birdnet_log birdnet_stats)
########################################
# Notification settings
########################################
NOTIFICATION_INTERVAL=1800 # 30 minutes in seconds
NOTIFICATION_INTERVAL_IN_MINUTES=$(( NOTIFICATION_INTERVAL / 60 ))
last_notification_time=0
issue_reported=0 # 1 = an issue was reported, 0 = system is normal
declare -A SERVICE_INACTIVE_COUNT=()
# Disk usage threshold (percentage)
DISK_USAGE_THRESHOLD=95
# "Analyzing" file check variables
same_file_counter=0
SAME_FILE_THRESHOLD=2
if [[ -f "$ANALYZING_NOW_FILE" ]]; then
analyzing_now=$(<"$ANALYZING_NOW_FILE")
else
analyzing_now=""
fi
########################################
# Notification Functions
########################################
apprisealert() { apprisealert() {
local notification="" local issue_message="$1"
local stopped_service="<br><b>Stopped services:</b> " local current_time
current_time=$(date +%s)
# Check for stopped services # Calculate time_diff in minutes since last notification
services=(birdnet_analysis chart_viewer spectrogram_viewer icecast2 birdnet_recording birdnet_log birdnet_stats) local time_diff=$(( (current_time - last_notification_time) / 60 ))
for service in "${services[@]}"; do
if [[ "$(systemctl is-active "$service")" == "inactive" ]]; then # 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; " stopped_service+="$service; "
fi fi
done done
# Build notification message local notification="<b>Issue:</b> $issue_message"
notification+="$stopped_service" notification+="$stopped_service"
notification+="<br><b>Additional information</b>: "
notification+="<br><b>Since:</b> ${LASTCHECK:-unknown}"
notification+="<br><b>System:</b> ${SITE_NAME:-$(hostname)}" notification+="<br><b>System:</b> ${SITE_NAME:-$(hostname)}"
notification+="<br>Available disk space: $(df -h "$HOME/BirdSongs" | awk 'NR==2 {print $4}')" 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>" [[ -n "$BIRDNETPI_URL" ]] && notification+="<br><a href=\"$BIRDNETPI_URL\">Access your BirdNET-Pi</a>"
# Send notification local TITLE="BirdNET-Analyzer Alert"
TITLE="BirdNET-Analyzer stopped" 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" "$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
} }
# Main loop apprisealert_recovery() {
while true; do # Only send a recovery message if we had previously reported an issue
sleep 61 if (( issue_reported == 1 )); then
log_green "$(date) INFO: System is back to normal. Sending recovery notification."
# Restart analysis if clogged local TITLE="BirdNET-Pi System Recovered"
if ((counter <= 0)); then local notification="<b>All monitored services are back to normal.</b><br>"
current_file="$(cat "$ingest_dir/analyzing_now.txt")" notification+="<b>System:</b> ${SITE_NAME:-$(hostname)}<br>"
if [[ "$current_file" == "$analyzing_now" ]]; then notification+="Available disk space: $(df -h "$BIRDSONGS_DIR" | awk 'NR==2 {print $4}')"
log_yellow "$(date) WARNING: no change in analyzing_now for 10 iterations, restarting services"
"$HOME/BirdNET-Pi/scripts/restart_services.sh" 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 fi
counter=10 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" analyzing_now="$current_file"
fi fi
# Check recorder state and queue length if (( same_file_counter >= SAME_FILE_THRESHOLD )); then
wav_count=$(find "$ingest_dir" -maxdepth 1 -name '*.wav' | wc -l) log_red "$(date) INFO: 'analyzing_now' file unchanged for $SAME_FILE_THRESHOLD iterations."
service_state=$(systemctl is-active "$srv") apprisealert "No change in analyzing_now for ${SAME_FILE_THRESHOLD} iterations"
analysis_state=$(systemctl is-active "$srv2") "$HOME/BirdNET-Pi/scripts/restart_services.sh"
same_file_counter=0
log_green "$(date) INFO: $wav_count wav files waiting in $ingest_dir, $srv state is $service_state, $srv2 state is $analysis_state" return 1
else
# Pause recorder if queue is too large # Only log if it changed this iteration
if ((wav_count > 50)); then if (( same_file_counter == 0 )); then
log_red "$(date) WARNING: Too many files in queue, pausing $srv and restarting $srv2" log_green "$(date) INFO: 'analyzing_now' file has been updated."
sudo systemctl stop "$srv" fi
sudo systemctl restart "$srv2" return 0
sleep 30
elif ((wav_count > 30)); then
log_red "$(date) WARNING: Too many files in queue, restarting $srv2"
sudo systemctl restart "$srv2"
sleep 30
fi fi
}
# Check service states check_queue() {
for service in "$srv" "$srv2"; do local wav_count
state_var="${service}_state" wav_count=$(find -L "$INGEST_DIR" -maxdepth 1 -name '*.wav' | wc -l)
if [[ "${state_var:-}" != "active" ]]; then
log_yellow "$(date) INFO: Restarting $service service" log_green "$(date) INFO: Queue is at a manageable level (${wav_count} wav files)."
sudo systemctl restart "$service"
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 fi
done done
# Send alert if needed if (( any_inactive == 0 )); then
if ((wav_count > 30)) && [[ -s "$HOME/BirdNET-Pi/apprise.txt" ]]; then log_green "$(date) INFO: All services are active"
apprisealert return 0
else
log_red "$(date) INFO: One or more services are inactive"
return 1
fi fi
}
((counter--)) ########################################
# Main Monitoring Loop
########################################
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
# 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 done

View File

@@ -1,6 +1,16 @@
#!/usr/bin/with-contenv bashio #!/usr/bin/with-contenv bashio
# shellcheck shell=bash # 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 if [ -d "$HOME"/BirdSongs/StreamData ]; then
bashio::log.fatal "Container stopping, saving temporary files." bashio::log.fatal "Container stopping, saving temporary files."
@@ -18,16 +28,22 @@ if [ -d "$HOME"/BirdSongs/StreamData ]; then
# Wait for both services to stop # Wait for both services to stop
wait wait
# Check if there are files in StreamData and move them to /data/StreamData # Create the destination directory
mkdir -p /data/StreamData mkdir -p /data/StreamData
if [ "$(ls -A "$HOME"/BirdSongs/StreamData)" ]; then
if mv -v "$HOME"/BirdSongs/StreamData/* /data/StreamData/; then # Move only valid WAV files under 50MB
bashio::log.info "Files successfully moved to /data/StreamData." 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" /data/StreamData/; then
bashio::log.info "Moved valid WAV file: $(basename "$file")"
else
bashio::log.error "Failed to move: $(basename "$file")"
fi
else else
bashio::log.error "Failed to move files to /data/StreamData." bashio::log.warning "Skipping invalid or large file: $(basename "$file")"
exit 1
fi fi
fi done
bashio::log.info "... files safe, allowing container to stop." bashio::log.info "... files safe, allowing container to stop."
else else

View File

@@ -2,6 +2,17 @@
# shellcheck shell=bash # shellcheck shell=bash
set -e 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 # # SET /CONFIG #
############### ###############
@@ -18,8 +29,6 @@ for file in "${DEFAULT_FILES[@]}"; do
done done
touch /config/include_species_list.txt # Ensure this is always created touch /config/include_species_list.txt # Ensure this is always created
touch "$HOME/BirdNET-Pi/scripts/common.php"
# Set BirdSongs folder location from configuration if specified # Set BirdSongs folder location from configuration if specified
BIRDSONGS_FOLDER="/config/BirdSongs" BIRDSONGS_FOLDER="/config/BirdSongs"
if bashio::config.has_value "BIRDSONGS_FOLDER"; then if bashio::config.has_value "BIRDSONGS_FOLDER"; then
@@ -53,6 +62,14 @@ echo "... setting permissions for user pi"
chown -R pi:pi /config /etc/birdnet "$BIRDSONGS_FOLDER" /tmp chown -R pi:pi /config /etc/birdnet "$BIRDSONGS_FOLDER" /tmp
chmod -R 755 /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 # Create default birds.db
if [ ! -f /config/birds.db ]; then if [ ! -f /config/birds.db ]; then
echo "... creating initial db" echo "... creating initial db"
@@ -65,12 +82,6 @@ elif [ "$(stat -c%s /config/birds.db)" -lt 10240 ]; then
cp "$HOME/BirdNET-Pi/scripts/birds.db" /config/ cp "$HOME/BirdNET-Pi/scripts/birds.db" /config/
fi fi
# Backup default birdnet.conf for sanity check
if [ ! -f "$HOME/BirdNET-Pi/birdnet.conf" ]; then
ln -s /etc/birdnet/birdnet.conf "$HOME/BirdNET-Pi/birdnet.conf"
fi
cp "$HOME/BirdNET-Pi/birdnet.conf" "$HOME/BirdNET-Pi/birdnet.bak"
# Symlink configuration files # Symlink configuration files
echo "... creating symlinks for configuration files" echo "... creating symlinks for configuration files"
CONFIG_FILES=("$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") CONFIG_FILES=("$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")
@@ -84,12 +95,6 @@ for file in "${CONFIG_FILES[@]}"; do
sudo -u pi ln -fs "/config/$filename" "/etc/birdnet/$filename" sudo -u pi ln -fs "/config/$filename" "/etc/birdnet/$filename"
done done
# thisrun
cp /config/birdnet.conf "$HOME/BirdNET-Pi/scripts/thisrun.txt"
cp /config/birdnet.conf "$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 BirdSongs folders # Symlink BirdSongs folders
for folder in By_Date Charts; do for folder in By_Date Charts; do
echo "... creating symlink for $BIRDSONGS_FOLDER/$folder" echo "... creating symlink for $BIRDSONGS_FOLDER/$folder"
@@ -102,9 +107,9 @@ echo "... checking and setting permissions"
chmod -R 755 /config/* chmod -R 755 /config/*
chmod 777 /config chmod 777 /config
# Create Matplotlib configuration directory # Create folder for matplotlib
echo "... setting up Matplotlabdir" echo "... setting up Matplotlabdir"
MPLCONFIGDIR="${MPLCONFIGDIR:-$HOME/.config/matplotlib}" mkdir -p "$HOME"/.cache/matplotlib
mkdir -p "$MPLCONFIGDIR" chown -R "pi:pi" "$HOME"/.cache/matplotlib
chown pi:pi "$MPLCONFIGDIR" chmod 777 "$HOME"/.cache/matplotlib
chmod 777 "$MPLCONFIGDIR"

View File

@@ -2,22 +2,38 @@
# shellcheck shell=bash # shellcheck shell=bash
set -e set -e
if [ -d /data/StreamData ]; then ##################
bashio::log.fatal "Container was stopped while files were still being analyzed." # ALLOW RESTARTS #
##################
# Check if there are .wav files in /data/StreamData if [[ "${BASH_SOURCE[0]}" == /etc/cont-init.d/* ]]; then
if find /data/StreamData -type f -name "*.wav" | grep -q .; then mkdir -p /etc/scripts-init
bashio::log.warning "Restoring .wav files from /data/StreamData to $HOME/BirdSongs/StreamData." 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 # Create the destination directory if it does not exist
mkdir -p "$HOME"/BirdSongs/StreamData mkdir -p "$HOME"/BirdSongs/StreamData
# Count the number of .wav files to be moved # Count the number of .wav files to be moved
file_count=$(find /data/StreamData -type f -name "*.wav" | wc -l) file_count=$(find /config/TemporaryFiles -type f -name "*.wav" | wc -l)
echo "... found $file_count .wav files to restore." echo "... found $file_count .wav files to restore."
# Move the .wav files using `mv` to avoid double log entries # Move the .wav files using `mv` to avoid double log entries
mv -v /data/StreamData/*.wav "$HOME"/BirdSongs/StreamData/ mv -v /config/TemporaryFiles/*.wav "$HOME"/BirdSongs/StreamData/
rm -r /config/TemporaryFiles
# Update permissions only if files were moved successfully # Update permissions only if files were moved successfully
if [ "$file_count" -gt 0 ]; then if [ "$file_count" -gt 0 ]; then
@@ -30,7 +46,7 @@ if [ -d /data/StreamData ]; then
fi fi
# Clean up the source folder if it is empty # Clean up the source folder if it is empty
if [ -z "$(ls -A /data/StreamData)" ]; then if [ -z "$(ls -A /config/TemporaryFiles)" ]; then
rm -r /data/StreamData rm -r /config/TemporaryFiles
fi fi
fi fi

View File

@@ -2,6 +2,17 @@
# shellcheck shell=bash # shellcheck shell=bash
set -e 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 # # CHECK BIRDNET.CONF #
###################### ######################
@@ -48,17 +59,17 @@ fi
bashio::log.info "Performing potential updates" bashio::log.info "Performing potential updates"
# Adapt update_birdnet_snippets # Adapt update_birdnet_snippets
sed -i "/USER=/a USER=\"$USER\"" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh sed -i "s|systemctl list-unit-files|false \&\& echo|g" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Avoid systemctl
sed -i "/HOME=/a HOME=\"$HOME\"" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh sed -i "/systemctl /d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Avoid systemctl
sed -i "/chown/d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh sed -i "/install_tmp_mount/d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh # Use HA tmp
sed -i "/chmod/d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh 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 "s|/etc/birdnet/birdnet.conf|/config/birdnet.conf|g" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh
sed -i "/restart_services/d" "$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
sed -i "/set -x/d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh
sed -i "s|systemctl list-unit-files|false \&\& echo|g" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh
sed -i "/systemctl /d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh
sed -i "/find /d" "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh
# Execute update_birdnet_snippets # 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 "$HOME"/BirdNET-Pi/scripts/update_birdnet_snippets.sh

View File

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

View File

@@ -0,0 +1,66 @@
#!/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
################
# ADD FEATURES #
################
bashio::log.info "Adding optional features"
# Enable the Processed folder
#############################
if bashio::config.true "PROCESSED_FOLDER_ENABLED" && ! grep -q "processed_size" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py; then
echo "... Enabling the Processed folder : the last 15 wav files will be stored there"
# Adapt config.php
sed -i "/GET\[\"info_site\"\]/a\ \$processed_size = \$_GET\[\"processed_size\"\];" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\$contents = file_get_contents/a\ \$contents = preg_replace\(\"/PROCESSED_SIZE=\.\*/\", \"PROCESSED_SIZE=\$processed_size\", \$contents\);" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i <table class=\"settingstable\"><tr><td>" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i <h2>Processed folder management </h2>" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i <label for=\"processed_size\">Amount of files to keep after analysis :</label>" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i <input name=\"processed_size\" type=\"number\" style=\"width:6em;\" max=\"90\" min=\"0\" step=\"1\" value=\"<\?php print(\$config\['PROCESSED_SIZE'\]);?>\"/>" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i </td></tr><tr><td>" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i Processed is the directory where the formerly 'Analyzed' files are moved after extractions, mostly for troubleshooting purposes.<br>" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i This value defines the maximum amount of files that are kept before replacement with new files.<br>" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i </td></tr></table>" "$HOME"/BirdNET-Pi/scripts/config.php
sed -i "/\"success\"/i\ <br>" "$HOME"/BirdNET-Pi/scripts/config.php
# Adapt birdnet_analysis.py - move_to_processed
sed -i "/log.info('handle_reporting_queue done')/a\ os.remove(files.pop(0))" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ while len(files) > processed_size:" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ files.sort(key=os.path.getmtime)" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ files = glob.glob(os.path.join(processed_dir, '*'))" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ os.rename(file_name, os.path.join(processed_dir, os.path.basename(file_name)))" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ processed_dir = os.path.join(get_settings()['RECS_DIR'], 'Processed')" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\def move_to_processed(file_name, processed_size):" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ " "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
# Adapt birdnet_analysis.py - get_processed_size
sed -i "/log.info('handle_reporting_queue done')/a\ return 0" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ except (ValueError, TypeError):" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ return processed_size if isinstance(processed_size, int) else 0" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ processed_size = get_settings().getint('PROCESSED_SIZE')" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ try:" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\def get_processed_size():" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/log.info('handle_reporting_queue done')/a\ " "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
# Modify calls
sed -i "/from subprocess import CalledProcessError/a\import glob" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/from subprocess import CalledProcessError/a\import time" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
# Modify main code
sed -i "/os.remove(file.file_name)/i\ processed_size = get_processed_size()" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/os.remove(file.file_name)/i\ if processed_size > 0:" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/os.remove(file.file_name)/i\ move_to_processed(file.file_name, processed_size)" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/os.remove(file.file_name)/i\ else:" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/os.remove(file.file_name)/c\ os.remove(file.file_name)" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
fi || true

View File

@@ -1,7 +1,18 @@
#!/command/with-contenv bashio #!/command/with-contenv bashio
# shellcheck shell=bash # shellcheck shell=bash disable=SC2016
set -e 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 # # MODIFY WEBUI #
################ ################
@@ -14,74 +25,115 @@ sed -i '/>System Controls/d' "$HOME/BirdNET-Pi/homepage/views.php"
# Remove Ram drive option from webui # Remove Ram drive option from webui
echo "... removing Ram drive from webui as it is handled from HA" echo "... removing Ram drive from webui as it is handled from HA"
sed -i '/Ram drive/{n;s/center"/center" style="display: none;"/;}' "$HOME/BirdNET-Pi/scripts/service_controls.php" if grep -q "Ram drive" "$HOME/BirdNET-Pi/scripts/service_controls.php"; then
sed -i '/Ram drive/d' "$HOME/BirdNET-Pi/scripts/service_controls.php" 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
# Correct services to start as user pi # Correct services to start as user pi
echo "... updating services to start as user pi" echo "... updating services to start as user pi"
while IFS= read -r file; do if ! grep -q "/usr/bin/sudo" "$HOME/BirdNET-Pi/templates/birdnet_log.service"; then
if [[ "$(basename "$file")" != "birdnet_log.service" ]]; then while IFS= read -r file; do
sed -i "s|ExecStart=|ExecStart=/usr/bin/sudo -u pi |g" "$file" if [[ "$(basename "$file")" != "birdnet_log.service" ]]; then
fi sed -i "s|ExecStart=|ExecStart=/usr/bin/sudo -u pi |g" "$file"
done < <(find "$HOME/BirdNET-Pi/templates/" -name "b*.service" -print) fi
done < <(find "$HOME/BirdNET-Pi/templates/" -name "birdnet*.service" -print)
fi
# Send services log to container logs # Send services log to container logs
echo "... redirecting services logs to container logs" echo "... redirecting services logs to container logs"
while IFS= read -r file; do while IFS= read -r file; do
sed -i "/Service/a StandardError=append:/proc/1/fd/1" "$file" sed -i "/StandardError/d" "$file"
sed -i "/Service/a StandardOutput=append:/proc/1/fd/1" "$file" sed -i "/StandardOutput/d" "$file"
done < <(find "$HOME/BirdNET-Pi/templates/" -name "b*.service" -print) 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)
# Avoid preselection in include and exclude lists # Avoid preselection in include and exclude lists
echo "... disabling preselecting options in include and exclude lists" echo "... disabling preselecting options in include and exclude lists"
sed -i "s|option selected|option disabled|g" "$HOME/BirdNET-Pi/scripts/include_list.php" sed -i "s|option selected|option disabled|g" "$HOME/BirdNET-Pi/scripts/include_list.php"
sed -i "s|option selected|option disabled|g" "$HOME/BirdNET-Pi/scripts/exclude_list.php" sed -i "s|option selected|option disabled|g" "$HOME/BirdNET-Pi/scripts/exclude_list.php"
# Preencode API key
if ! 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 # Correct log services to show /proc/1/fd/1
echo "... redirecting birdnet_log service output to /logs" echo "... redirecting birdnet_log service output to /logs"
sed -i "/User=pi/d" "$HOME/BirdNET-Pi/templates/birdnet_log.service" 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" sed -i "s|birdnet_log.sh|cat /proc/1/fd/1|g" "$HOME/BirdNET-Pi/templates/birdnet_log.service"
# Correct backup script
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"
# Caddyfile modifications # Caddyfile modifications
echo "... modifying Caddyfile configurations" echo "... modifying Caddyfile configurations"
caddy fmt --overwrite /etc/caddy/Caddyfile caddy fmt --overwrite /etc/caddy/Caddyfile
#Change port to leave 80 free for certificate requests #Change port to leave 80 free for certificate requests
sed -i "s|http://|http://:8081|g" /etc/caddy/Caddyfile if ! grep -q "http://:8081" /etc/caddy/Caddyfile; then
sed -i "s|http://|http://:8081|g" "$HOME/BirdNET-Pi/scripts/update_caddyfile.sh" sed -i "s|http://|http://:8081|g" /etc/caddy/Caddyfile
if [ -f /etc/caddy/Caddyfile.original ]; then sed -i "s|http://|http://:8081|g" "$HOME/BirdNET-Pi/scripts/update_caddyfile.sh"
rm /etc/caddy/Caddyfile.original if [ -f /etc/caddy/Caddyfile.original ]; then
rm /etc/caddy/Caddyfile.original
fi
fi fi
# Correct webui paths # Correct webui paths
echo "... correcting webui paths" echo "... correcting webui paths"
sed -i "s|/stats|/stats/|g" "$HOME/BirdNET-Pi/homepage/views.php" if ! grep -q "/stats/" "$HOME/BirdNET-Pi/homepage/views.php"; then
sed -i "s|/log|/log/|g" "$HOME/BirdNET-Pi/homepage/views.php" 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
# Check if port 80 is correctly configured # Check if port 80 is correctly configured
if [ -n "$(bashio::addon.port 80)" ] && [ "$(bashio::addon.port 80)" != 80 ]; then if [ -n "$(bashio::addon.port "80")" ] && [ "$(bashio::addon.port "80")" != 80 ]; then
bashio::log.fatal "The port 80 is enabled, but should still be 80 if you want automatic SSL certificates generation to work." bashio::log.fatal "The port 80 is enabled, but should still be 80 if you want automatic SSL certificates generation to work."
fi fi
# Correct systemctl path # Correct systemctl path
echo "... updating systemctl path" #echo "... updating systemctl path"
mv /helpers/systemctl3.py /bin/systemctl #if [[ -f /helpers/systemctl3.py ]]; then
chmod a+x /bin/systemctl # mv /helpers/systemctl3.py /bin/systemctl
# chmod a+x /bin/systemctl
#fi
# Improve streamlit cache
#echo "... add streamlit cache"
#sed -i "/def get_data/i \\@st\.cache_resource\(\)" "$HOME/BirdNET-Pi/scripts/plotly_streamlit.py"
# 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
echo ".. add highpass and lowpass to sox extracts"
sed -i "s|f'={stop}']|f'={stop}', 'highpass', '250', 'lowpass', '15000']|g" "$HOME/BirdNET-Pi/scripts/utils/reporting.py"
sed -i '/sox.*-V1/s/spectrogram/highpass 250 spectrogram/' "$HOME/BirdNET-Pi/scripts/spectrogram.sh"
# Correct timedatectl path # Correct timedatectl path
echo "updating timedatectl path" echo "updating timedatectl path"
mv /helpers/timedatectl /usr/bin/timedatectl if [[ -f /helpers/timedatectl ]]; then
chown pi:pi /usr/bin/timedatectl mv /helpers/timedatectl /usr/bin/timedatectl
chmod a+x /usr/bin/timedatectl chown pi:pi /usr/bin/timedatectl
chmod a+x /usr/bin/timedatectl
fi
# Correct timezone showing in config.php # Correct timezone showing in config.php
# shellcheck disable=SC2016
echo "... updating timezone in config.php"
sed -i -e '/<option disabled selected>/s/selected//' \ sed -i -e '/<option disabled selected>/s/selected//' \
-e '/\$current_timezone = trim(shell_exec("timedatectl show --value --property=Timezone"));/d' \ -e '/\$current_timezone = trim(shell_exec("timedatectl show --value --property=Timezone"));/d' \
-e "/\$date = new DateTime('now');/i \$current_timezone = trim(shell_exec(\"timedatectl show --value --property=Timezone\"));" \ -e "/\$date = new DateTime('now');/i \$current_timezone = trim(shell_exec(\"timedatectl show --value --property=Timezone\"));" \
-e "/\$date = new DateTime('now');/i date_default_timezone_set(\$current_timezone);" "$HOME/BirdNET-Pi/scripts/config.php" -e "/\$date = new DateTime('now');/i date_default_timezone_set(\$current_timezone);" "$HOME/BirdNET-Pi/scripts/config.php"
# Use only first user # Correct language labels according to birdnet.conf
echo "... correcting for multiple users" echo "... adapting labels according to birdnet.conf"
for file in $(grep -rl "/1000/{print" "$HOME"/BirdNET-Pi/scripts); do if export "$(grep "^DATABASE_LANG" /config/birdnet.conf)"; then
sed -i "s|'/1000/{print \$1}'|'/1000/{print \$1; exit}'|" "$file" bashio::log.info "Setting language to ${DATABASE_LANG:-en}"
sed -i "s|'/1000/{print \$6}'|'/1000/{print \$6; exit}'|" "$file" "$HOME/BirdNET-Pi/scripts/install_language_label_nm.sh" -l "${DATABASE_LANG:-}" &>/dev/null || bashio::log.warning "Failed to update language labels"
done else
bashio::log.warning "DATABASE_LANG not found in configuration. Using default labels."
fi

View File

@@ -2,6 +2,17 @@
# shellcheck shell=bash # shellcheck shell=bash
set -e 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 # # NGINX SETTING #
################# #################
@@ -12,8 +23,9 @@ ingress_interface=$(bashio::addon.ip_address)
ingress_entry=$(bashio::addon.ingress_entry) ingress_entry=$(bashio::addon.ingress_entry)
# Quits if ingress is not active # Quits if ingress is not active
if [ -z "$ingress_entry" ]; then if [[ "$ingress_entry" != "/api"* ]]; then
bashio::log.warning "Ingress entry is not set, exiting configuration." bashio::log.info "Ingress entry is not set, exiting configuration."
sed -i "1a sleep infinity" /custom-services.d/02-nginx.sh
exit 0 exit 0
fi fi
@@ -31,14 +43,20 @@ else
exit 1 exit 1
fi fi
# Disable log
sed -i "/View Log/d" "$HOME/BirdNET-Pi/homepage/views.php"
echo "... ensuring restricted area access" echo "... ensuring restricted area access"
echo "${ingress_entry}" > /ingress_url echo "${ingress_entry}" > /ingress_url
# Modify PHP file safely # Modify PHP file safely
for php_file in config.php play.php advanced.php overview.php; do php_file="$HOME/BirdNET-Pi/scripts/common.php"
sed -i "s|if (\!isset(\$_SERVER\['PHP_AUTH_USER'\])) {|if (\!isset(\$_SERVER\['PHP_AUTH_USER'\]) \&\& strpos(\$_SERVER\['HTTP_REFERER'\], '/api/hassio_ingress') == false) {|g" "$HOME/BirdNET-Pi/scripts/$php_file" if [ -f "$php_file" ]; then
sed -i "s+if(\$submittedpwd == \$caddypwd \&\& \$submitteduser == 'birdnet')+if((\$submittedpwd == \$caddypwd \&\& \$submitteduser == 'birdnet') || (strpos(\$_SERVER['HTTP_REFERER'], '/api/hassio_ingress') !== false \&\& strpos(\$_SERVER['HTTP_REFERER'], trim(file_get_contents('/ingress_url'))) !== false)+g" "$HOME/BirdNET-Pi/scripts/$php_file" 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"
done else
bashio::log.error "PHP file not found: $php_file"
exit 1
fi
echo "... adapting Caddyfile for ingress" echo "... adapting Caddyfile for ingress"
chmod +x /helpers/caddy_ingress.sh chmod +x /helpers/caddy_ingress.sh

View File

@@ -2,6 +2,17 @@
# shellcheck shell=bash # shellcheck shell=bash
set -e 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 # # SSL SETTING #
############### ###############

View File

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

View File

@@ -1,18 +1,32 @@
#!/command/with-contenv bashio #!/command/with-contenv bashio
# shellcheck shell=bash # shellcheck shell=bash
set -e
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 SYSTEM #
############## ##############
# Set password
bashio::log.info "Setting password for the user pi" bashio::log.info "Setting password for the user pi"
echo "pi:$(bashio::config "pi_password")" | chpasswd if bashio::config.has_value "pi_password"; then
echo "pi:$(bashio::config "pi_password")" | chpasswd
fi
bashio::log.info "Password set successfully for user pi." bashio::log.info "Password set successfully for user pi."
bashio::log.info "Setting timezone :"
# Use timezone defined in add-on options if available # Use timezone defined in add-on options if available
bashio::log.info "Setting timezone :"
if bashio::config.has_value 'TZ'; then if bashio::config.has_value 'TZ'; then
TZ_VALUE="$(bashio::config 'TZ')" TZ_VALUE="$(bashio::config 'TZ')"
if timedatectl set-timezone "$TZ_VALUE"; then if timedatectl set-timezone "$TZ_VALUE"; then
@@ -38,7 +52,11 @@ else
else else
bashio::log.fatal "Couldn't set automatic timezone! Please set a manual one from the options." bashio::log.fatal "Couldn't set automatic timezone! Please set a manual one from the options."
fi fi
fi fi || 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 system services"
@@ -53,11 +71,12 @@ chmod +x "$HOME/BirdNET-Pi/scripts/restart_services.sh" >/dev/null
"$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 # Start livestream services if enabled in configuration
if bashio::config.true LIVESTREAM_BOOT_ENABLED; then if bashio::config.true "LIVESTREAM_BOOT_ENABLED"; then
echo "... starting livestream services" echo "... starting livestream services"
systemctl enable icecast2 >/dev/null systemctl enable icecast2 >/dev/null
systemctl start icecast2.service >/dev/null systemctl start icecast2.service >/dev/null
systemctl enable --now livestream.service >/dev/null systemctl enable --now livestream.service >/dev/null
fi fi
bashio::log.info "Setup complete." # Start
bashio::log.info "✅ Setup complete."

View File

@@ -1,4 +1,4 @@
server { server {
listen %%interface%%:%%port%% default_server; listen %%interface%%:%%port%% default_server;
include /etc/nginx/includes/server_params.conf; include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf; include /etc/nginx/includes/proxy_params.conf;
@@ -6,21 +6,6 @@
proxy_buffering off; proxy_buffering off;
auth_basic_user_file /home/pi/.htpasswd; auth_basic_user_file /home/pi/.htpasswd;
location /log {
# Proxy pass
proxy_pass http://localhost:8082;
}
location /stats {
# Proxy pass
proxy_pass http://localhost:8082;
}
location /terminal {
# Proxy pass
proxy_pass http://localhost:8082;
}
location / { location / {
# Proxy pass # Proxy pass
proxy_pass http://localhost:8082; proxy_pass http://localhost:8082;
@@ -30,6 +15,12 @@
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "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 # Correct base_url
proxy_set_header Accept-Encoding ""; proxy_set_header Accept-Encoding "";
sub_filter_once off; sub_filter_once off;
@@ -37,11 +28,14 @@
sub_filter /spectrogram %%ingress_entry%%/spectrogram; sub_filter /spectrogram %%ingress_entry%%/spectrogram;
sub_filter /By_Date/ %%ingress_entry%%/By_Date/; sub_filter /By_Date/ %%ingress_entry%%/By_Date/;
sub_filter /Charts/ %%ingress_entry%%/Charts/; 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 /todays %%ingress_entry%%/todays;
sub_filter href=\"/ href=\"%%ingress_entry%%/; sub_filter href=\"/ href=\"%%ingress_entry%%/;
sub_filter src=\"/ src=\"%%ingress_entry%%/; sub_filter src=\"/ src=\"%%ingress_entry%%/;
sub_filter hx-get=\"/ hx-get=\"%%ingress_entry%%/; sub_filter hx-get=\"/ hx-get=\"%%ingress_entry%%/;
sub_filter action=\"/ action=\"%%ingress_entry%%/; sub_filter action=\"/ action=\"%%ingress_entry%%/;
} }
} }

View File

@@ -51,8 +51,8 @@ def get_bird_code(scientific_name):
def automatic_mqtt_publish(file, detection, path): def automatic_mqtt_publish(file, detection, path):
bird = {} bird = {}
bird['Date'] = file.date bird['Date'] = detection.date
bird['Time'] = file.time bird['Time'] = detection.time
bird['ScientificName'] = detection.scientific_name.replace('_', ' ') bird['ScientificName'] = detection.scientific_name.replace('_', ' ')
bird['CommonName'] = detection.common_name bird['CommonName'] = detection.common_name
bird['Confidence'] = detection.confidence bird['Confidence'] = detection.confidence
@@ -86,7 +86,7 @@ def automatic_mqtt_publish(file, detection, path):
json_bird = json.dumps(bird) json_bird = json.dumps(bird)
mqttc.reconnect() mqttc.reconnect()
mqttc.publish(mqtt_topic, json_bird, 1) mqttc.publish(mqtt_topic, json_bird, 1)
log.info("Posted to MQTT: ok") log.info("Posted to MQTT: ok")
mqttc = mqtt.Client('birdnet_mqtt') mqttc = mqtt.Client('birdnet_mqtt')

View File

@@ -1,116 +0,0 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
</style>
<p><strong>This tool will allow to convert on-the-fly species to compensate for model errors. It SHOULD NOT BE USED except if you know what you are doing, instead the model errors should be reported to the owner. However, it is still convenient for systematic biases that are confirmed through careful listening of samples, while waiting for the models to be updated.</strong></p>
<div class="customlabels column1">
<form action="" method="GET" id="add">
<input type="hidden" id="species" name="species">
<h3>Specie to convert from :</h3>
<!-- Input box to filter options in the first table -->
<input type="text" id="species1Search" onkeyup="filterOptions('species1')" placeholder="Search for species...">
<select name="species1" id="species1" size="25">
<?php
error_reporting(E_ALL);
ini_set('display_errors',1);
$filename = './scripts/labels.txt';
$eachline = file($filename, FILE_IGNORE_NEW_LINES);
foreach($eachline as $lines){echo
"<option value=\"".$lines."\">$lines</option>";}
?>
</select>
<br><br> <!-- Added a space between the two tables -->
<h3>Specie to convert to :</h3>
<!-- Input box to filter options in the second table -->
<input type="text" id="species2Search" onkeyup="filterOptions('species2')" placeholder="Search for species...">
<select name="species2" id="species2" size="25">
<?php
foreach($eachline as $lines){echo
"<option value=\"".$lines."\">$lines</option>";}
?>
</select>
<input type="hidden" name="add" value="add">
</form>
<div class="customlabels smaller">
<button type="submit" name="view" value="Converted" form="add">>>ADD>></button>
</div>
</div>
<div class="customlabels column2">
<table><td>
<button type="submit" name="view" value="Converted" form="add">>>ADD>></button>
<br><br>
<button type="submit" name="view" value="Converted" form="del">REMOVE</button>
</td></table>
</div>
<div class="customlabels column3" style="margin-top: 0;"> <!-- Removed the blank space above the table -->
<form action="" method="GET" id="del">
<h3>Converted Species List</h3>
<select name="species[]" id="value2" multiple size="25">
<?php
$filename = './scripts/convert_species_list.txt'; // Changed the file path
$eachline = file($filename, FILE_IGNORE_NEW_LINES);
foreach($eachline as $lines){
echo
"<option value=\"".$lines."\">$lines</option>";
}?>
</select>
<input type="hidden" name="del" value="del">
</form>
<div class="customlabels smaller">
<button type="submit" name="view" value="Converted" form="del">REMOVE</button>
</div>
</div>
<input type="hidden" id="hiddenSpecies" name="hiddenSpecies">
<script>
document.getElementById("add").addEventListener("submit", function(event) {
var speciesSelect1 = document.getElementById("species1");
var speciesSelect2 = document.getElementById("species2");
if (speciesSelect1.selectedIndex < 0 || speciesSelect2.selectedIndex < 0) {
alert("Please select a species from both lists.");
document.querySelector('.views').style.opacity = 1;
event.preventDefault();
} else {
var selectedSpecies1 = speciesSelect1.options[speciesSelect1.selectedIndex].value;
var selectedSpecies2 = speciesSelect2.options[speciesSelect2.selectedIndex].value;
document.getElementById("species").value = selectedSpecies1 + ";" + selectedSpecies2;
}
});
// Store the original list of options in a variable
var originalOptions = {};
// Function to filter options in a select element
function filterOptions(id) {
var input = document.getElementById(id + "Search");
var filter = input.value.toUpperCase();
var select = document.getElementById(id);
var options = select.getElementsByTagName("option");
// If the original list of options for this select element hasn't been stored yet, store it
if (!originalOptions[id]) {
originalOptions[id] = Array.from(options).map(option => option.value);
}
// Clear the select element
while (select.firstChild) {
select.removeChild(select.firstChild);
}
// Populate the select element with the filtered labels
originalOptions[id].forEach(label => {
if (label.toUpperCase().indexOf(filter) > -1) {
let option = document.createElement('option');
option.value = label;
option.text = label;
select.appendChild(option);
}
});
}
</script>

View File

@@ -0,0 +1,31 @@
#! /usr/bin/python3
import argparse
import os
import sys
parser = argparse.ArgumentParser()
parser.add_argument('-u', '--unit', metavar='unit', type=str, required=True, help='Systemd unit to display')
parser.add_argument('-f', '--follow', default=False, action='store_true', help='Follows the log')
parser.add_argument('-n', '--lines', metavar='num', type=int, help='Num of lines to display')
parser.add_argument('--no-pager', default=False, action='store_true', help='Do not pipe through a pager')
parser.add_argument('--system', default=False, action='store_true', help='Show system units')
parser.add_argument('--user', default=False, action='store_true', help='Show user units')
parser.add_argument('--root', metavar='path', type=str, help='Use subdirectory path')
parser.add_argument('-x', default=False, action='store_true', help='Switch on verbose mode')
args = parser.parse_args()
systemctl_py = "systemctl3.py"
path = os.path.dirname(sys.argv[0])
systemctl = os.path.join(path, systemctl_py)
cmd = [ systemctl, "log", args.unit ] # drops the -u
if args.follow: cmd += [ "-f" ]
if args.lines: cmd += [ "-n", str(args.lines) ]
if args.no_pager: cmd += [ "--no-pager" ]
if args.system: cmd += [ "--system" ]
elif args.user: cmd += [ "--user" ]
if args.root: cmd += [ "--root", start(args.root) ]
if args.x: cmd += [ "-vvv" ]
os.execvp(cmd[0], cmd)

View File

@@ -21,8 +21,8 @@ import fnmatch
import re import re
from types import GeneratorType from types import GeneratorType
__copyright__ = "(C) 2016-2024 Guido U. Draheim, licensed under the EUPL" __copyright__ = "(C) 2016-2025 Guido U. Draheim, licensed under the EUPL"
__version__ = "1.5.8066" __version__ = "1.5.9063"
# | # |
# | # |
@@ -561,9 +561,8 @@ def shutil_truncate(filename):
filedir = os.path.dirname(filename) filedir = os.path.dirname(filename)
if not os.path.isdir(filedir): if not os.path.isdir(filedir):
os.makedirs(filedir) os.makedirs(filedir)
f = open(filename, "w") with open(filename, "w") as f:
f.write("") f.write("")
f.close()
# http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid # http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
def pid_exists(pid): def pid_exists(pid):
@@ -615,9 +614,10 @@ def _pid_zombie(pid):
raise ValueError('invalid PID 0') raise ValueError('invalid PID 0')
check = _proc_pid_status.format(**locals()) check = _proc_pid_status.format(**locals())
try: try:
for line in open(check): with open(check) as f:
if line.startswith("State:"): for line in f:
return "Z" in line if line.startswith("State:"):
return "Z" in line
except IOError as e: except IOError as e:
if e.errno != errno.ENOENT: if e.errno != errno.ENOENT:
logg.error("%s (%s): %s", check, e.errno, e) logg.error("%s (%s): %s", check, e.errno, e)
@@ -770,46 +770,49 @@ class SystemctlConfigParser(SystemctlConfData):
name, text = "", "" name, text = "", ""
if os.path.isfile(filename): if os.path.isfile(filename):
self._files.append(filename) self._files.append(filename)
for orig_line in open(filename): with open(filename) as f:
if nextline: for orig_line in f:
text += orig_line if nextline:
if text.rstrip().endswith("\\") or text.rstrip().endswith("\\\n"): text += orig_line
text = text.rstrip() + "\n" if text.rstrip().endswith("\\") or text.rstrip().endswith("\\\n"):
text = text.rstrip() + "\n"
else:
self.set(section, name, text)
nextline = False
continue
line = orig_line.strip()
if not line:
continue
if line.startswith("#"):
continue
if line.startswith(";"):
continue
if line.startswith(".include"):
logg.error("the '.include' syntax is deprecated. Use x.service.d/ drop-in files!")
includefile = re.sub(r'^\.include[ ]*', '', line).rstrip()
if not os.path.isfile(includefile):
raise Exception("tried to include file that doesn't exist: %s" % includefile)
self.read_sysd(includefile)
continue
if line.startswith("["):
x = line.find("]")
if x > 0:
section = line[1:x]
self.add_section(section)
continue
m = re.match(r"(\w+) *=(.*)", line)
if not m:
logg.warning("bad ini line: %s", line)
raise Exception("bad ini line")
name, text = m.group(1), m.group(2).strip()
if text.endswith("\\") or text.endswith("\\\n"):
nextline = True
text = text + "\n"
else: else:
self.set(section, name, text) # hint: an empty line shall reset the value-list
nextline = False self.set(section, name, text and text or None)
continue if nextline:
line = orig_line.strip() self.set(section, name, text)
if not line:
continue
if line.startswith("#"):
continue
if line.startswith(";"):
continue
if line.startswith(".include"):
logg.error("the '.include' syntax is deprecated. Use x.service.d/ drop-in files!")
includefile = re.sub(r'^\.include[ ]*', '', line).rstrip()
if not os.path.isfile(includefile):
raise Exception("tried to include file that doesn't exist: %s" % includefile)
self.read_sysd(includefile)
continue
if line.startswith("["):
x = line.find("]")
if x > 0:
section = line[1:x]
self.add_section(section)
continue
m = re.match(r"(\w+) *=(.*)", line)
if not m:
logg.warning("bad ini line: %s", line)
raise Exception("bad ini line")
name, text = m.group(1), m.group(2).strip()
if text.endswith("\\") or text.endswith("\\\n"):
nextline = True
text = text + "\n"
else:
# hint: an empty line shall reset the value-list
self.set(section, name, text and text or None)
return self return self
def read_sysv(self, filename): def read_sysv(self, filename):
""" an LSB header is scanned and converted to (almost) """ an LSB header is scanned and converted to (almost)
@@ -819,20 +822,21 @@ class SystemctlConfigParser(SystemctlConfData):
section = "GLOBAL" section = "GLOBAL"
if os.path.isfile(filename): if os.path.isfile(filename):
self._files.append(filename) self._files.append(filename)
for orig_line in open(filename): with open(filename) as f:
line = orig_line.strip() for orig_line in f:
if line.startswith("#"): line = orig_line.strip()
if " BEGIN INIT INFO" in line: if line.startswith("#"):
initinfo = True if " BEGIN INIT INFO" in line:
section = "init.d" initinfo = True
if " END INIT INFO" in line: section = "init.d"
initinfo = False if " END INIT INFO" in line:
if initinfo: initinfo = False
m = re.match(r"\S+\s*(\w[\w_-]*):(.*)", line) if initinfo:
if m: m = re.match(r"\S+\s*(\w[\w_-]*):(.*)", line)
key, val = m.group(1), m.group(2).strip() if m:
self.set(section, key, val) key, val = m.group(1), m.group(2).strip()
continue self.set(section, key, val)
continue
self.systemd_sysv_generator(filename) self.systemd_sysv_generator(filename)
return self return self
def systemd_sysv_generator(self, filename): def systemd_sysv_generator(self, filename):
@@ -962,8 +966,9 @@ class PresetFile:
return None return None
def read(self, filename): def read(self, filename):
self._files.append(filename) self._files.append(filename)
for line in open(filename): with open(filename) as f:
self._lines.append(line.strip()) for line in f:
self._lines.append(line.strip())
return self return self
def get_preset(self, unit): def get_preset(self, unit):
for line in self._lines: for line in self._lines:
@@ -1834,10 +1839,11 @@ class Systemctl:
return default return default
try: try:
# some pid-files from applications contain multiple lines # some pid-files from applications contain multiple lines
for line in open(pid_file): with open(pid_file) as f:
if line.strip(): for line in f:
pid = to_intN(line.strip()) if line.strip():
break pid = to_intN(line.strip())
break
except Exception as e: except Exception as e:
logg.warning("bad read of pid file '%s': %s", pid_file, e) logg.warning("bad read of pid file '%s': %s", pid_file, e)
return pid return pid
@@ -1953,15 +1959,16 @@ class Systemctl:
return status return status
try: try:
if DEBUG_STATUS: logg.debug("reading %s", status_file) if DEBUG_STATUS: logg.debug("reading %s", status_file)
for line in open(status_file): with open(status_file) as f:
if line.strip(): for line in f:
m = re.match(r"(\w+)[:=](.*)", line) if line.strip():
if m: m = re.match(r"(\w+)[:=](.*)", line)
key, value = m.group(1), m.group(2) if m:
if key.strip(): key, value = m.group(1), m.group(2)
status[key.strip()] = value.strip() if key.strip():
else: # pragma: no cover status[key.strip()] = value.strip()
logg.warning("ignored %s", line.strip()) else: # pragma: no cover
logg.warning("ignored %s", line.strip())
except: except:
logg.warning("bad read of status file '%s'", status_file) logg.warning("bad read of status file '%s'", status_file)
return status return status
@@ -2060,7 +2067,6 @@ class Systemctl:
assert isinstance(line, bytes) assert isinstance(line, bytes)
if line.startswith(b"btime"): if line.startswith(b"btime"):
system_btime = float(line.decode().split()[1]) system_btime = float(line.decode().split()[1])
f.closed
if DEBUG_BOOTTIME: if DEBUG_BOOTTIME:
logg.debug(" BOOT 2. System btime secs: %.3f (%s)", system_btime, system_stat) logg.debug(" BOOT 2. System btime secs: %.3f (%s)", system_btime, system_stat)
@@ -2113,22 +2119,23 @@ class Systemctl:
logg.debug("file does not exist: %s", real_file) logg.debug("file does not exist: %s", real_file)
return return
try: try:
for real_line in open(os_path(self._root, env_file)): with open(os_path(self._root, env_file)) as f:
line = real_line.strip() for real_line in f:
if not line or line.startswith("#"): line = real_line.strip()
continue if not line or line.startswith("#"):
m = re.match(r"(?:export +)?([\w_]+)[=]'([^']*)'", line) continue
if m: m = re.match(r"(?:export +)?([\w_]+)[=]'([^']*)'", line)
yield m.group(1), m.group(2) if m:
continue yield m.group(1), m.group(2)
m = re.match(r'(?:export +)?([\w_]+)[=]"([^"]*)"', line) continue
if m: m = re.match(r'(?:export +)?([\w_]+)[=]"([^"]*)"', line)
yield m.group(1), m.group(2) if m:
continue yield m.group(1), m.group(2)
m = re.match(r'(?:export +)?([\w_]+)[=](.*)', line) continue
if m: m = re.match(r'(?:export +)?([\w_]+)[=](.*)', line)
yield m.group(1), m.group(2) if m:
continue yield m.group(1), m.group(2)
continue
except Exception as e: except Exception as e:
logg.info("while reading %s: %s", env_file, e) logg.info("while reading %s: %s", env_file, e)
def read_env_part(self, env_part): # -> generate[ (name, value) ] def read_env_part(self, env_part): # -> generate[ (name, value) ]
@@ -5293,18 +5300,18 @@ class Systemctl:
logg.error(" %s: %s has no ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.", unit, section) logg.error(" %s: %s has no ExecStart= setting, which is only allowed for Type=oneshot services. Refusing.", unit, section)
errors += 101 errors += 101
if len(usedExecStart) > 1 and haveType != "oneshot": if len(usedExecStart) > 1 and haveType != "oneshot":
logg.error(" %s: there may be only one %s ExecStart statement (unless for 'oneshot' services)." logg.error(" %s: there may be only one %s ExecStart statement (unless for 'oneshot' services)." +
+ "\n\t\t\tYou can use ExecStartPre / ExecStartPost to add additional commands.", unit, section) "\n\t\t\tYou can use ExecStartPre / ExecStartPost to add additional commands.", unit, section)
errors += 1 errors += 1
if len(usedExecStop) > 1 and haveType != "oneshot": if len(usedExecStop) > 1 and haveType != "oneshot":
logg.info(" %s: there should be only one %s ExecStop statement (unless for 'oneshot' services)." logg.info(" %s: there should be only one %s ExecStop statement (unless for 'oneshot' services)." +
+ "\n\t\t\tYou can use ExecStopPost to add additional commands (also executed on failed Start)", unit, section) "\n\t\t\tYou can use ExecStopPost to add additional commands (also executed on failed Start)", unit, section)
if len(usedExecReload) > 1: if len(usedExecReload) > 1:
logg.info(" %s: there should be only one %s ExecReload statement." logg.info(" %s: there should be only one %s ExecReload statement." +
+ "\n\t\t\tUse ' ; ' for multiple commands (ExecReloadPost or ExedReloadPre do not exist)", unit, section) "\n\t\t\tUse ' ; ' for multiple commands (ExecReloadPost or ExedReloadPre do not exist)", unit, section)
if len(usedExecReload) > 0 and "/bin/kill " in usedExecReload[0]: if len(usedExecReload) > 0 and "/bin/kill " in usedExecReload[0]:
logg.warning(" %s: the use of /bin/kill is not recommended for %s ExecReload as it is asynchronous." logg.warning(" %s: the use of /bin/kill is not recommended for %s ExecReload as it is asynchronous." +
+ "\n\t\t\tThat means all the dependencies will perform the reload simultaneously / out of order.", unit, section) "\n\t\t\tThat means all the dependencies will perform the reload simultaneously / out of order.", unit, section)
if conf.getlist(Service, "ExecRestart", []): # pragma: no cover if conf.getlist(Service, "ExecRestart", []): # pragma: no cover
logg.error(" %s: there no such thing as an %s ExecRestart (ignored)", unit, section) logg.error(" %s: there no such thing as an %s ExecRestart (ignored)", unit, section)
if conf.getlist(Service, "ExecRestartPre", []): # pragma: no cover if conf.getlist(Service, "ExecRestartPre", []): # pragma: no cover
@@ -5961,7 +5968,7 @@ class Systemctl:
interval = conf.get(Service, "StartLimitIntervalSec", strE(defaults)) # 10s interval = conf.get(Service, "StartLimitIntervalSec", strE(defaults)) # 10s
return time_to_seconds(interval, maximum) return time_to_seconds(interval, maximum)
def get_RestartSec(self, conf, maximum = None): def get_RestartSec(self, conf, maximum = None):
maximum = maximum or DefaultStartLimitIntervalSec maximum = maximum or DefaultMaximumTimeout
delay = conf.get(Service, "RestartSec", strE(DefaultRestartSec)) delay = conf.get(Service, "RestartSec", strE(DefaultRestartSec))
return time_to_seconds(delay, maximum) return time_to_seconds(delay, maximum)
def restart_failed_units(self, units, maximum = None): def restart_failed_units(self, units, maximum = None):
@@ -6186,11 +6193,12 @@ class Systemctl:
zombie = False zombie = False
ppid = -1 ppid = -1
try: try:
for line in open(proc_status): with open(proc_status) as f:
m = re.match(r"State:\s*Z.*", line) for line in f:
if m: zombie = True m = re.match(r"State:\s*Z.*", line)
m = re.match(r"PPid:\s*(\d+)", line) if m: zombie = True
if m: ppid = int(m.group(1)) m = re.match(r"PPid:\s*(\d+)", line)
if m: ppid = int(m.group(1))
except IOError as e: except IOError as e:
logg.warning("%s : %s", proc_status, e) logg.warning("%s : %s", proc_status, e)
continue continue
@@ -6263,13 +6271,14 @@ class Systemctl:
proc_status = _proc_pid_status.format(**locals()) proc_status = _proc_pid_status.format(**locals())
if os.path.isfile(proc_status): if os.path.isfile(proc_status):
try: try:
for line in open(proc_status): with open(proc_status) as f:
if line.startswith("PPid:"): for line in f:
ppid_text = line[len("PPid:"):].strip() if line.startswith("PPid:"):
try: ppid = int(ppid_text) ppid_text = line[len("PPid:"):].strip()
except: continue try: ppid = int(ppid_text)
if ppid in pidlist and pid not in pids: except: continue
pids += [pid] if ppid in pidlist and pid not in pids:
pids += [pid]
except IOError as e: except IOError as e:
logg.warning("%s : %s", proc_status, e) logg.warning("%s : %s", proc_status, e)
continue continue
@@ -6302,7 +6311,8 @@ class Systemctl:
if pid: if pid:
try: try:
cmdline = _proc_pid_cmdline.format(**locals()) cmdline = _proc_pid_cmdline.format(**locals())
cmd = open(cmdline).read().split("\0") with open(cmdline) as f:
cmd = f.read().split("\0")
if DEBUG_KILLALL: logg.debug("cmdline %s", cmd) if DEBUG_KILLALL: logg.debug("cmdline %s", cmd)
found = None found = None
cmd_exe = os.path.basename(cmd[0]) cmd_exe = os.path.basename(cmd[0])
@@ -6333,33 +6343,33 @@ class Systemctl:
logg.debug("checking hosts sysconf for '::1 localhost'") logg.debug("checking hosts sysconf for '::1 localhost'")
lines = [] lines = []
sysconf_hosts = os_path(self._root, _etc_hosts) sysconf_hosts = os_path(self._root, _etc_hosts)
for line in open(sysconf_hosts): with open(sysconf_hosts) as f:
if "::1" in line: for line in f:
newline = re.sub("\\slocalhost\\s", " ", line) if "::1" in line:
if line != newline: newline = re.sub("\\slocalhost\\s", " ", line)
logg.info("%s: '%s' => '%s'", _etc_hosts, line.rstrip(), newline.rstrip()) if line != newline:
line = newline logg.info("%s: '%s' => '%s'", _etc_hosts, line.rstrip(), newline.rstrip())
lines.append(line) line = newline
f = open(sysconf_hosts, "w") lines.append(line)
for line in lines: with open(sysconf_hosts, "w") as f:
f.write(line) for line in lines:
f.close() f.write(line)
def force_ipv6(self, *args): def force_ipv6(self, *args):
""" only ipv4 localhost in /etc/hosts """ """ only ipv4 localhost in /etc/hosts """
logg.debug("checking hosts sysconf for '127.0.0.1 localhost'") logg.debug("checking hosts sysconf for '127.0.0.1 localhost'")
lines = [] lines = []
sysconf_hosts = os_path(self._root, _etc_hosts) sysconf_hosts = os_path(self._root, _etc_hosts)
for line in open(sysconf_hosts): with open(sysconf_hosts) as f:
if "127.0.0.1" in line: for line in f:
newline = re.sub("\\slocalhost\\s", " ", line) if "127.0.0.1" in line:
if line != newline: newline = re.sub("\\slocalhost\\s", " ", line)
logg.info("%s: '%s' => '%s'", _etc_hosts, line.rstrip(), newline.rstrip()) if line != newline:
line = newline logg.info("%s: '%s' => '%s'", _etc_hosts, line.rstrip(), newline.rstrip())
lines.append(line) line = newline
f = open(sysconf_hosts, "w") lines.append(line)
for line in lines: with open(sysconf_hosts, "w") as f:
f.write(line) for line in lines:
f.close() f.write(line)
def help_modules(self, *args): def help_modules(self, *args):
"""[command] -- show this help """[command] -- show this help
""" """
@@ -6523,7 +6533,12 @@ def print_str_dict_dict(result):
logg.log(HINT, "EXEC END %i items", shown) logg.log(HINT, "EXEC END %i items", shown)
logg.debug(" END %s", result) logg.debug(" END %s", result)
def run(command, *modules): def runcommand(command, *modules):
systemctl = Systemctl()
if FORCE_IPV4:
systemctl.force_ipv4()
elif FORCE_IPV6:
systemctl.force_ipv6()
exitcode = 0 exitcode = 0
if command in ["help"]: if command in ["help"]:
print_str_list(systemctl.help_modules(*modules)) print_str_list(systemctl.help_modules(*modules))
@@ -6770,6 +6785,8 @@ if __name__ == "__main__":
_only_type = opt.only_type _only_type = opt.only_type
_only_property = opt.only_property _only_property = opt.only_property
_only_what = opt.only_what _only_what = opt.only_what
FORCE_IPV4 = opt.ipv4
FORCE_IPV6 = opt.ipv6
# being PID 1 (or 0) in a container will imply --init # being PID 1 (or 0) in a container will imply --init
_pid = os.getpid() _pid = os.getpid()
_init = opt.init or _pid in [1, 0] _init = opt.init or _pid in [1, 0]
@@ -6829,7 +6846,6 @@ if __name__ == "__main__":
# #
print_begin(sys.argv, args) print_begin(sys.argv, args)
# #
systemctl = Systemctl()
if opt.version: if opt.version:
args = ["version"] args = ["version"]
if not args: if not args:
@@ -6844,8 +6860,4 @@ if __name__ == "__main__":
modules.remove("service") modules.remove("service")
except ValueError: except ValueError:
pass pass
if opt.ipv4: sys.exit(runcommand(command, *modules))
systemctl.force_ipv4()
elif opt.ipv6:
systemctl.force_ipv6()
sys.exit(run(command, *modules))

View File

@@ -75,7 +75,7 @@ show_time_details() {
local_time="$(date)" local_time="$(date)"
utc_time="$(date -u)" utc_time="$(date -u)"
time_zone="$(show_timezone)" time_zone="$(show_timezone)"
# Check if NTP is used # Check if NTP is used
if systemctl is-active --quiet systemd-timesyncd; then if systemctl is-active --quiet systemd-timesyncd; then
ntp_status="yes" ntp_status="yes"

View File

@@ -1,27 +0,0 @@
if($_GET['view'] == "Converted"){
ensure_authenticated();
if(isset($_GET['species']) && isset($_GET['add'])){
$file = './scripts/convert_species_list.txt';
$str = file_get_contents("$file");
$str = preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $str);
file_put_contents("$file", "$str");
// Write $_GET['species'] to the file
file_put_contents("./scripts/convert_species_list.txt", htmlspecialchars_decode($_GET['species'], ENT_QUOTES)."\n", FILE_APPEND);
} elseif (isset($_GET['species']) && isset($_GET['del'])){
$file = './scripts/convert_species_list.txt';
$str = file_get_contents("$file");
$str = preg_replace('/^\h*\v+/m', '', $str);
file_put_contents("$file", "$str");
foreach($_GET['species'] as $selectedOption) {
$content = file_get_contents("./scripts/convert_species_list.txt");
$newcontent = str_replace($selectedOption, "", "$content");
$newcontent = str_replace(htmlspecialchars_decode($selectedOption, ENT_QUOTES), "", "$content");
file_put_contents("./scripts/convert_species_list.txt", "$newcontent");
}
$file = './scripts/convert_species_list.txt';
$str = file_get_contents("$file");
$str = preg_replace('/^\h*\v+/m', '', $str);
file_put_contents("$file", "$str");
}
include('./scripts/convert_list.php');
}