Files
PADD/padd.sh
Christian Boyle 95ffbd5bf9 fix dhcp display in padd tiny
updates from review

more updates from review

fix dhcp string to checkbox

fix dhcp ip range display

fix dhcp ip range display again

enhance dhcp range display

Update padd.sh

Co-authored-by: RD WebDesign <github@rdwebdesign.com.br>
Signed-off-by: Christian Boyle <christianboyle@users.noreply.github.com>

add newline at end of file, per editorconfig-checker failure

Update padd.sh

Co-authored-by: yubiuser <github@yubiuser.dev>
Signed-off-by: Christian Boyle <christianboyle@users.noreply.github.com>
2025-03-29 21:35:37 +01:00

1913 lines
78 KiB
Bash
Executable File
Raw Blame History

#!/usr/bin/env sh
# shellcheck disable=SC1091
# Ignore warning about `local` being undefinded in POSIX
# shellcheck disable=SC3043
# https://github.com/koalaman/shellcheck/wiki/SC3043#exceptions
# PADD
# A more advanced version of the chronometer provided with Pihole
# SETS LOCALE
export LC_ALL=C
export LC_NUMERIC=C
############################################ VARIABLES #############################################
# VERSION
padd_version="v4.0.0"
# LastChecks
LastCheckPADDInformation=$(date +%s)
LastCheckFullInformation=$(date +%s)
LastCheckNetworkInformation=$(date +%s)
# padd_data holds the data returned by FTL's /padd endpoint globally
padd_data=""
# COLORS
CSI="$(printf '\033')[" # Control Sequence Introducer
red_text="${CSI}91m" # Red
green_text="${CSI}92m" # Green
yellow_text="${CSI}93m" # Yellow
blue_text="${CSI}94m" # Blue
magenta_text="${CSI}95m" # Magenta
cyan_text="${CSI}96m" # Cyan
reset_text="${CSI}0m" # Reset to default
clear_line="${CSI}0K" # Clear the current line to the right to wipe any artifacts remaining from last print
# STYLES
bold_text="${CSI}1m"
blinking_text="${CSI}5m"
dim_text="${CSI}2m"
# CHECK BOXES
check_box_good="[${green_text}${reset_text}]" # Good
check_box_bad="[${red_text}${reset_text}]" # Bad
check_box_disabled="[${blue_text}-${reset_text}]" # Disabled, but not an error
check_box_question="[${yellow_text}?${reset_text}]" # Question / ?
check_box_info="[${yellow_text}i${reset_text}]" # Info / i
# PICO STATUSES
pico_status_ok="${check_box_good} Sys. OK"
pico_status_update="${check_box_info} Update"
pico_status_hot="${check_box_bad} Sys. Hot!"
pico_status_off="${check_box_info} No blck"
pico_status_ftl_down="${check_box_bad} No CXN"
pico_status_dns_down="${check_box_bad} DNS Down"
# MINI STATUS
mini_status_ok="${check_box_good} System OK"
mini_status_update="${check_box_info} Update avail."
mini_status_hot="${check_box_bad} System is hot!"
mini_status_off="${check_box_info} No blocking!"
mini_status_ftl_down="${check_box_bad} No connection!"
mini_status_dns_down="${check_box_bad} DNS off!"
# REGULAR STATUS
full_status_ok="${check_box_good} System is healthy"
full_status_update="${check_box_info} Updates are available"
full_status_hot="${check_box_bad} System is hot!"
full_status_off="${check_box_info} Blocking is disabled"
full_status_ftl_down="${check_box_bad} No connection!"
full_status_dns_down="${check_box_bad} DNS is off!"
# MEGA STATUS
mega_status_ok="${check_box_good} Your system is healthy"
mega_status_update="${check_box_info} Updates are available"
mega_status_hot="${check_box_bad} Your system is hot!"
mega_status_off="${check_box_info} Blocking is disabled!"
mega_status_ftl_down="${check_box_bad} No connection to FTL!"
mega_status_dns_down="${check_box_bad} Pi-hole's DNS server is off!"
# TINY STATUS
tiny_status_ok="${check_box_good} System is healthy"
tiny_status_update="${check_box_info} Updates are available"
tiny_status_hot="${check_box_bad} System is hot!"
tiny_status_off="${check_box_info} Blocking is disabled"
tiny_status_ftl_down="${check_box_bad} No connection to FTL!"
tiny_status_dns_down="${check_box_bad} DNS is off!"
# Text only "logos"
padd_text="${green_text}${bold_text}PADD${reset_text}"
# PADD logos - regular and retro
padd_logo_1="${bold_text}${green_text} __ __ __ ${reset_text}"
padd_logo_2="${bold_text}${green_text}|__) /\\ | \\| \\ ${reset_text}"
padd_logo_3="${bold_text}${green_text}| /--\\|__/|__/ ${reset_text}"
padd_logo_retro_1="${bold_text} ${yellow_text}_${green_text}_ ${blue_text}_${magenta_text}_ ${yellow_text}_${green_text}_ ${reset_text}"
padd_logo_retro_2="${bold_text}${yellow_text}|${green_text}_${blue_text}_${cyan_text}) ${red_text}/${yellow_text}\\ ${blue_text}| ${red_text}\\${yellow_text}| ${cyan_text}\\ ${reset_text}"
padd_logo_retro_3="${bold_text}${green_text}| ${red_text}/${yellow_text}-${green_text}-${blue_text}\\${cyan_text}|${magenta_text}_${red_text}_${yellow_text}/${green_text}|${blue_text}_${cyan_text}_${magenta_text}/ ${reset_text}"
############################################# FTL ##################################################
TestAPIAvailability() {
local chaos_api_list authResponse cmdResult digReturnCode authStatus authData
# Query the API URLs from FTL using CHAOS TXT
# The result is a space-separated enumeration of full URLs
# e.g., "http://localhost:80/api" or "https://domain.com:443/api"
if [ -z "${SERVER}" ] || [ "${SERVER}" = "localhost" ] || [ "${SERVER}" = "127.0.0.1" ]; then
# --server was not set or set to local, assuming we're running locally
cmdResult="$(dig +short chaos txt local.api.ftl @localhost 2>&1; echo $?)"
else
# --server was set, try to get response from there
cmdResult="$(dig +short chaos txt domain.api.ftl @"${SERVER}" 2>&1; echo $?)"
fi
# Gets the return code of the dig command (last line)
# We can't use${cmdResult##*$'\n'*} here as $'..' is not POSIX
digReturnCode="$(echo "${cmdResult}" | tail -n 1)"
if [ ! "${digReturnCode}" = "0" ]; then
# If the query was not successful
moveXOffset; echo "API not available. Please check server address and connectivity"
exit 1
else
# Dig returned 0 (success), so get the actual response (first line)
chaos_api_list="$(echo "${cmdResult}" | head -n 1)"
fi
# Iterate over space-separated list of URLs
while [ -n "${chaos_api_list}" ]; do
# Get the first URL
API_URL="${chaos_api_list%% *}"
# Strip leading and trailing quotes
API_URL="${API_URL%\"}"
API_URL="${API_URL#\"}"
# Test if the API is available at this URL
authResponse=$(curl --connect-timeout 2 -skS -w "%{http_code}" "${API_URL}auth")
# authStatus are the last 3 characters
# not using ${authResponse#"${authResponse%???}"}" here because it's extremely slow on big responses
authStatus=$(printf "%s" "${authResponse}" | tail -c 3)
# data is everything from response without the last 3 characters
authData=$(printf %s "${authResponse%???}")
# Test if http status code was 200 (OK) or 401 (authentication required)
if [ ! "${authStatus}" = 200 ] && [ ! "${authStatus}" = 401 ]; then
# API is not available at this port/protocol combination
API_PORT=""
else
# API is available at this URL combination
if [ "${authStatus}" = 200 ]; then
# API is available without authentication
needAuth=false
fi
# Check if 2FA is required
needTOTP=$(echo "${authData}"| jq --raw-output .session.totp 2>/dev/null)
break
fi
# Remove the first URL from the list
local last_api_list
last_api_list="${chaos_api_list}"
chaos_api_list="${chaos_api_list#* }"
# If the list did not change, we are at the last element
if [ "${last_api_list}" = "${chaos_api_list}" ]; then
# Remove the last element
chaos_api_list=""
fi
done
# if API_PORT is empty, no working API port was found
if [ -n "${API_PORT}" ]; then
moveXOffset; echo "API not available at: ${API_URL}"
moveXOffset; echo "Exiting."
exit 1
fi
}
LoginAPI() {
# Exit early if no authentication is required
if [ "${needAuth}" = false ]; then
moveXOffset; echo "No authentication required."
return
fi
# Try to read the CLI password (if enabled and readable by the current user)
if [ -r /etc/pihole/cli_pw ]; then
password=$(cat /etc/pihole/cli_pw)
# If we can read the CLI password, we can skip 2FA even when it's required otherwise
needTOTP=false
fi
if [ -z "${password}" ]; then
# no password was supplied as argument or read from CLI file
moveXOffset; echo "No password supplied. Please enter your password:"
# secretly read the password
moveXOffset; secretRead; printf '\n'
fi
if [ "${needTOTP}" = true ] && [ -z "${totp}" ]; then
# 2FA required, but no TOTP was supplied as argument
moveXOffset; echo "Please enter the correct second factor."
moveXOffset; echo "(Can be any number if you used the app password)"
moveXOffset; read -r totp
fi
# Try to authenticate using the supplied password (CLI file, argument or user input) and TOTP
Authenticate
# Try to login again until the session is valid
while [ ! "${validSession}" = true ] ; do
moveXOffset; echo "Authentication failed."
# Print the error message if there is one
if [ ! "${sessionError}" = "null" ]; then
moveXOffset; echo "Error: ${sessionError}"
fi
# Print the session message if there is one
if [ ! "${sessionMessage}" = "null" ]; then
moveXOffset; echo "Error: ${sessionMessage}"
fi
moveXOffset; echo "Please enter the correct password:"
# secretly read the password
moveXOffset; secretRead; printf '\n'
if [ "${needTOTP}" = true ]; then
moveXOffset; echo "Please enter the correct second factor:"
moveXOffset; echo "(Can be any number if you used the app password)"
moveXOffset; read -r totp
fi
# Try to authenticate again
Authenticate
done
# Loop exited, authentication was successful
moveXOffset; echo "Authentication successful."
}
DeleteSession() {
# if a valid Session exists (no password required or successful authenthication) and
# SID is not null (successful authenthication only), delete the session
if [ "${validSession}" = true ] && [ ! "${SID}" = null ]; then
# Try to delete the session. Omit the output, but get the http status code
deleteResponse=$(curl --connect-timeout 2 -skS -o /dev/null -w "%{http_code}" -X DELETE "${API_URL}auth" -H "Accept: application/json" -H "sid: ${SID}")
printf "\n\n"
case "${deleteResponse}" in
"204") moveXOffset; printf "%b" "Session successfully deleted.\n";;
"401") moveXOffset; printf "%b" "Logout attempt without a valid session. Unauthorized!\n";;
esac;
else
# no session to delete, just print a newline for nicer output
echo
fi
}
Authenticate() {
sessionResponse="$(curl --connect-timeout 2 -skS -X POST "${API_URL}auth" --user-agent "PADD ${padd_version}" --data "{\"password\":\"${password}\", \"totp\":${totp:-null}}" )"
if [ -z "${sessionResponse}" ]; then
moveXOffset; echo "No response from FTL server. Please check connectivity and use the options to set the API URL"
moveXOffset; echo "Usage: $0 [--server <domain|IP>]"
exit 1
fi
# obtain validity, session ID and sessionMessage from session response
validSession=$(echo "${sessionResponse}"| jq .session.valid 2>/dev/null)
SID=$(echo "${sessionResponse}"| jq --raw-output .session.sid 2>/dev/null)
sessionMessage=$(echo "${sessionResponse}"| jq --raw-output .session.message 2>/dev/null)
# obtain the error message from the session response
sessionError=$(echo "${sessionResponse}"| jq --raw-output .error.message 2>/dev/null)
}
GetFTLData() {
local response
local data
local status
# get the data from querying the API as well as the http status code
response=$(curl --connect-timeout 2 -sk -w "%{http_code}" -X GET "${API_URL}$1$2" -H "Accept: application/json" -H "sid: ${SID}" )
# status are the last 3 characters
# not using ${response#"${response%???}"}" here because it's extremely slow on big responses
status=$(printf "%s" "${response}" | tail -c 3)
# data is everything from response without the last 3 characters
data=$(printf %s "${response%???}")
if [ "${status}" = 200 ]; then
echo "${data}"
elif [ "${status}" = 000 ]; then
# connection lost
echo "000"
elif [ "${status}" = 401 ]; then
# unauthorized
echo "401"
fi
}
############################################# GETTERS ##############################################
GetPADDData() {
local response
response=$(GetFTLData "padd" "$1")
if [ "${response}" = 000 ]; then
# connection lost
padd_data="000"
elif [ "${response}" = 401 ]; then
# unauthorized
padd_data="401"
else
# Iterate over all the leaf paths in the JSON object and creates key-value
# pairs in the format "key=value". Nested objects are flattened using the dot
# notation, e.g., { "a": { "b": 1 } } becomes "a.b=1".
# We cannot use leaf_paths here as it was deprecated in jq 1.6 and removed in
# current master
# Using "paths(scalars | true)" will return null and false values.
# We also check if the value is exactly `null` and, in this case, return the
# string "null", as jq would return an empty string for nulls.
padd_data=$(echo "$response" | jq -r 'paths(scalars | true) as $p | [$p | join(".")] + [if getpath($p)!=null then getpath($p) else "null" end] | join("=")' 2>/dev/null)
fi
}
GetPADDValue() {
echo "$padd_data" | sed -n "s/^$1=//p" 2>/dev/null
}
GetSummaryInformation() {
if [ "${connection_down_flag}" = true ]; then
clients="N/A"
blocking_enabled="N/A"
domains_being_blocked="N/A"
dns_queries_today="N/A"
ads_blocked_today="N/A"
ads_percentage_today="N/A"
cache_size="N/A"
cache_evictions="N/A"
cache_inserts="N/A"
latest_blocked_raw="N/A"
top_blocked_raw="N/A"
top_domain_raw="N/A"
top_client_raw="N/A"
return
fi
clients=$(GetPADDValue active_clients)
blocking_enabled=$(GetPADDValue blocking)
domains_being_blocked_raw=$(GetPADDValue gravity_size)
domains_being_blocked=$(printf "%.f" "${domains_being_blocked_raw}")
dns_queries_today_raw=$(GetPADDValue queries.total)
dns_queries_today=$(printf "%.f" "${dns_queries_today_raw}")
ads_blocked_today_raw=$(GetPADDValue queries.blocked)
ads_blocked_today=$(printf "%.f" "${ads_blocked_today_raw}")
ads_percentage_today_raw=$(GetPADDValue queries.percent_blocked)
ads_percentage_today=$(printf "%.1f" "${ads_percentage_today_raw}")
cache_size=$(GetPADDValue cache.size)
cache_evictions=$(GetPADDValue cache.evicted)
cache_inserts=$(echo "${padd_data}"| GetPADDValue cache.inserted)
latest_blocked_raw=$(GetPADDValue recent_blocked)
top_blocked_raw=$(GetPADDValue top_blocked)
top_domain_raw=$(GetPADDValue top_domain)
top_client_raw=$(GetPADDValue top_client)
}
GetSystemInformation() {
if [ "${connection_down_flag}" = true ]; then
system_uptime_raw=0
temperature="N/A"
temp_heatmap=${reset_text}
cpu_load_1="N/A"
cpu_load_5="N/A"
cpu_load_15="N/A"
cpu_load_1_heatmap=${reset_text}
cpu_load_5_heatmap=${reset_text}
cpu_load_15_heatmap=${reset_text}
cpu_percent=0
memory_percent=0
memory_heatmap=${reset_text}
sys_model="N/A"
return
fi
# System uptime
system_uptime_raw=$(GetPADDValue system.uptime)
# CPU temperature and unit
cpu_temp_raw=$(GetPADDValue sensors.cpu_temp)
if [ $cpu_temp_raw != null ]; then
cpu_temp=$(printf "%.1f" "${cpu_temp_raw}")
temp_unit=$(echo "${padd_data}" | GetPADDValue sensors.unit)
fi
# Temp + Unit
if [ "${temp_unit}" = "C" ]; then
temperature="${cpu_temp}°${temp_unit}"
# no conversion needed
cpu_temp_celsius="$(echo "${cpu_temp}" | awk -F '.' '{print $1}')"
temp_unicode=true
elif [ "${temp_unit}" = "F" ]; then
temperature="${cpu_temp}°${temp_unit}"
# convert to Celsius for limit checking
cpu_temp_celsius="$(echo "${cpu_temp}" | awk '{print ($1-32) * 5 / 9}' | awk -F '.' '{print $1}')"
temp_unicode=true
elif [ "${temp_unit}" = "K" ]; then
# no ° for Kelvin
temperature="${cpu_temp}${temp_unit}"
# convert to Celsius for limit checking
cpu_temp_celsius="$(echo "${cpu_temp}" | awk '{print $1 - 273.15}' | awk -F '.' '{print $1}')"
temp_unicode=false
else # unknown unit
temperature="N/A"
# no conversion needed
cpu_temp_celsius=-274
temp_unicode=false
fi
# CPU temperature heatmap
hot_flag=false
# If we're getting close to 85°C... (https://www.raspberrypi.org/blog/introducing-turbo-mode-up-to-50-more-performance-for-free/)
if [ "${cpu_temp_celsius}" -gt 80 ]; then
temp_heatmap=${blinking_text}${red_text}
# set flag to change the status message in SetStatusMessage()
hot_flag=true
elif [ "${cpu_temp_celsius}" -gt 70 ]; then
temp_heatmap=${magenta_text}
elif [ "${cpu_temp_celsius}" -gt 60 ]; then
temp_heatmap=${blue_text}
elif [ "${cpu_temp_celsius}" -gt -274 ]; then
temp_heatmap=${cyan_text}
else
temp_heatmap=${clear_text}
fi
# CPU, load, heatmap
core_count=$(GetPADDValue system.cpu.nprocs)
cpu_load_1=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[0])")
cpu_load_5=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[1])")
cpu_load_15=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[2])")
cpu_load_1_heatmap=$(HeatmapGenerator "${cpu_load_1}" "${core_count}")
cpu_load_5_heatmap=$(HeatmapGenerator "${cpu_load_5}" "${core_count}")
cpu_load_15_heatmap=$(HeatmapGenerator "${cpu_load_15}" "${core_count}")
cpu_percent=$(printf %.1f "$(GetPADDValue system.cpu.load.percent.0)")
# Memory use, heatmap and bar
memory_percent_raw="$(GetPADDValue system.memory.ram.%used)"
memory_percent=$(printf %.1f "${memory_percent_raw}")
memory_heatmap="$(HeatmapGenerator "${memory_percent}")"
# Get device model
sys_model="$(GetPADDValue host_model)"
# DOCKER_VERSION is set during GetVersionInformation, so this needs to run first during startup
if [ ! "${DOCKER_VERSION}" = "null" ]; then
# Docker image
sys_model="Container"
fi
# Cleaning device model from useless OEM information
sys_model=$(filterModel "${sys_model}")
# FTL returns null if device information is not available
if [ -z "$sys_model" ] || [ "$sys_model" = "null" ]; then
sys_model="N/A"
fi
}
GetNetworkInformation() {
if [ "${connection_down_flag}" = true ]; then
iface_name="N/A"
pi_ip4_addr="N/A"
pi_ip6_addr="N/A"
ipv6_status="N/A"
ipv6_heatmap=${reset_text}
ipv6_check_box=${check_box_question}
dhcp_status="N/A"
dhcp_heatmap=${reset_text}
dhcp_range="N/A"
dhcp_range_heatmap=${reset_text}
dhcp_ipv6_status="N/A"
dhcp_ipv6_heatmap=${reset_text}
pi_hostname="N/A"
full_hostname="N/A"
dns_count="N/A"
dns_information="N/A"
dnssec_status="N/A"
dnssec_heatmap=${reset_text}
conditional_forwarding_status="N/A"
conditional_forwarding_heatmap=${reset_text}
tx_bytes="N/A"
rx_bytes="N/A"
return
fi
gateway_v4_iface=$(GetPADDValue iface.v4.name)
gateway_v6_iface=$(GetPADDValue iface.v4.name)
# Get IPv4 address of the default interface
pi_ip4_addrs="$(GetPADDValue iface.v4.num_addrs)"
pi_ip4_addr="$(GetPADDValue iface.v4.addr)"
if [ "${pi_ip4_addrs}" -eq 0 ]; then
# No IPv4 address available
pi_ip4_addr="N/A"
elif [ "${pi_ip4_addrs}" -eq 1 ]; then
# One IPv4 address available
: # Do nothing as the address is already set
else
# More than one IPv4 address available
pi_ip4_addr="${pi_ip4_addr}+"
fi
# Get IPv6 address of the default interface
pi_ip6_addrs="$(GetPADDValue iface.v6.num_addrs)"
pi_ip6_addr="$(GetPADDValue iface.v6.addr)"
if [ "${pi_ip6_addrs}" -eq 0 ]; then
# No IPv6 address available
pi_ip6_addr="N/A"
ipv6_check_box=${check_box_disabled}
ipv6_status="Disabled"
ipv6_heatmap=${blue_text}
elif [ "${pi_ip6_addrs}" -eq 1 ]; then
# One IPv6 address available
ipv6_check_box=${check_box_good}
ipv6_status="Enabled"
ipv6_heatmap=${green_text}
else
# More than one IPv6 address available
pi_ip6_addr="${pi_ip6_addr}+"
ipv6_check_box=${check_box_good}
ipv6_status="Enabled"
ipv6_heatmap=${green_text}
fi
# Is Pi-Hole acting as the DHCP server?
DHCP_ACTIVE="$(GetPADDValue config.dhcp_active )"
if [ "${DHCP_ACTIVE}" = "true" ]; then
DHCP_START="$(GetPADDValue config.dhcp_start)"
DHCP_END="$(GetPADDValue config.dhcp_end)"
dhcp_status="Enabled"
dhcp_range="${DHCP_START} - ${DHCP_END}"
dhcp_range_heatmap=${reset_text}
dhcp_heatmap=${green_text}
dhcp_check_box=${check_box_good}
# Is DHCP handling IPv6?
DHCP_IPv6="$(GetPADDValue config.dhcp_ipv6)"
if [ "${DHCP_IPv6}" = "true" ]; then
dhcp_ipv6_status="Enabled"
dhcp_ipv6_heatmap=${green_text}
else
dhcp_ipv6_status="Disabled"
dhcp_ipv6_heatmap=${blue_text}
fi
else
dhcp_status="Disabled"
dhcp_heatmap=${blue_text}
dhcp_check_box=${check_box_disabled}
dhcp_range="N/A"
dhcp_ipv6_status="N/A"
dhcp_range_heatmap=${yellow_text}
dhcp_ipv6_heatmap=${yellow_text}
fi
# Get hostname
pi_hostname="$(GetPADDValue node_name)"
full_hostname=${pi_hostname}
# when PI-hole is the DHCP server, append the domain to the hostname
if [ "${DHCP_ACTIVE}" = "true" ]; then
PIHOLE_DOMAIN="$(GetPADDValue config.dns_domain)"
if [ -n "${PIHOLE_DOMAIN}" ]; then
count=${pi_hostname}"."${PIHOLE_DOMAIN}
count=${#count}
if [ "${count}" -lt "18" ]; then
full_hostname=${pi_hostname}"."${PIHOLE_DOMAIN}
fi
fi
fi
# Get the number of configured upstream DNS servers
dns_count="$(GetPADDValue config.dns_num_upstreams)"
# if there's only one DNS server
if [ "${dns_count}" -eq 1 ]; then
dns_information="1 server"
else
dns_information="${dns_count} servers"
fi
# DNSSEC
DNSSEC="$(GetPADDValue config.dns_dnssec)"
if [ "${DNSSEC}" = "true" ]; then
dnssec_status="Enabled"
dnssec_heatmap=${green_text}
else
dnssec_status="Disabled"
dnssec_heatmap=${blue_text}
fi
# Conditional forwarding
CONDITIONAL_FORWARDING="$(GetPADDValue config.dns_revServer_active)"
if [ "${CONDITIONAL_FORWARDING}" = "true" ]; then
conditional_forwarding_status="Enabled"
conditional_forwarding_heatmap=${green_text}
else
conditional_forwarding_status="Disabled"
conditional_forwarding_heatmap=${blue_text}
fi
# Default interface data (use IPv4 interface - we cannot show both and assume they are the same)
iface_name="${gateway_v4_iface}"
tx_bytes="$(GetPADDValue iface.v4.tx_bytes.value)"
tx_bytes_unit="$(GetPADDValue iface.v4.tx_bytes.unit)"
tx_bytes=$(printf "%.1f %b" "${tx_bytes}" "${tx_bytes_unit}")
rx_bytes="$(GetPADDValue iface.v4.rx_bytes.value)"
rx_bytes_unit="$(GetPADDValue iface.v4.rx_bytes.unit)"
rx_bytes=$(printf "%.1f %b" "${rx_bytes}" "${rx_bytes_unit}")
# If IPv4 and IPv6 interfaces are not the same, add a "*" to the interface
# name to highlight that there are two different interfaces and the
# displayed statistics are only for the IPv4 interface, while the IPv6
# address correctly corresponds to the default IPv6 interface
if [ ! "${gateway_v4_iface}" = "${gateway_v6_iface}" ]; then
iface_name="${iface_name}*"
fi
}
GetPiholeInformation() {
if [ "${connection_down_flag}" = true ]; then
ftl_status="No connection"
ftl_heatmap=${red_text}
ftl_check_box=${check_box_bad}
ftl_cpu="N/A"
ftl_mem_percentage="N/A"
dns_status="DNS offline"
dns_heatmap=${red_text}
dns_check_box=${check_box_bad}
ftlPID="N/A"
dns_down_flag=true
return
fi
ftl_status="Running"
ftl_heatmap=${green_text}
ftl_check_box=${check_box_good}
# Get FTL CPU and memory usage
ftl_cpu_raw="$(GetPADDValue "%cpu")"
ftl_mem_percentage_raw="$(GetPADDValue "%mem")"
ftl_cpu="$(printf "%.1f" "${ftl_cpu_raw}")%"
ftl_mem_percentage="$(printf "%.1f" "${ftl_mem_percentage_raw}")%"
# Get Pi-hole (blocking) status
ftl_dns_port=$(GetPADDValue config.dns_port)
# Get FTL's current PID
ftlPID="$(GetPADDValue pid)"
# ${ftl_dns_port} == 0 DNS server part of dnsmasq disabled
dns_down_flag=false
if [ "${ftl_dns_port}" = 0 ]; then
dns_status="DNS offline"
dns_heatmap=${red_text}
dns_check_box=${check_box_bad}
# set flag to change the status message in SetStatusMessage()
dns_down_flag=true
else
dns_check_box=${check_box_good}
dns_status="Active"
dns_heatmap=${green_text}
fi
}
GetVersionInformation() {
if [ "${connection_down_flag}" = true ]; then
DOCKER_VERSION=null
CORE_VERSION="N/A"
WEB_VERSION="N/A"
FTL_VERSION="N/A"
core_version_heatmap=${reset_text}
web_version_heatmap=${reset_text}
ftl_version_heatmap=${reset_text}
return
fi
out_of_date_flag=false
# Gather DOCKER version information...
# returns "null" if not running Pi-hole in Docker container
DOCKER_VERSION="$(GetPADDValue version.docker.local)"
# If PADD is running inside docker, immediately return without checking for updated component versions
if [ ! "${DOCKER_VERSION}" = "null" ] ; then
GITHUB_DOCKER_VERSION="$(GetPADDValue version.docker.remote)"
docker_version_converted="$(VersionConverter "${DOCKER_VERSION}")"
docker_version_latest_converted="$(VersionConverter "${GITHUB_DOCKER_VERSION}")"
# Note: the version comparison will fail for any Docker tag not following a 'YYYY.MM.VV' scheme
# e.g. 'nightly', 'beta', 'v6-pre-alpha' and might set a false out_of_date_flag
# As those versions are not meant to be used in production, we ignore this small bug
if [ "${docker_version_converted}" -lt "${docker_version_latest_converted}" ]; then
out_of_date_flag="true"
docker_version_heatmap=${red_text}
else
docker_version_heatmap=${green_text}
fi
return
fi
# Gather core version information...
CORE_BRANCH="$(GetPADDValue version.core.local.branch)"
CORE_VERSION="$(GetPADDValue version.core.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')"
GITHUB_CORE_VERSION="$(GetPADDValue version.core.remote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')"
CORE_HASH="$(GetPADDValue version.core.local.hash)"
GITHUB_CORE_HASH="$(GetPADDValue version.core.remote.hash)"
if [ "${CORE_BRANCH}" = "master" ]; then
core_version_converted="$(VersionConverter "${CORE_VERSION}")"
core_version_latest_converted=$(VersionConverter "${GITHUB_CORE_VERSION}")
if [ "${core_version_converted}" -lt "${core_version_latest_converted}" ]; then
out_of_date_flag="true"
core_version_heatmap=${red_text}
else
core_version_heatmap=${green_text}
fi
else
# Custom branch
if [ -z "${CORE_BRANCH}" ]; then
# Branch name is empty, something went wrong
core_version_heatmap=${red_text}
CORE_VERSION="?"
else
if [ "${CORE_HASH}" = "${GITHUB_CORE_HASH}" ]; then
# up-to-date
core_version_heatmap=${green_text}
else
# out-of-date
out_of_date_flag="true"
core_version_heatmap=${red_text}
fi
# shorten common branch names (fix/, tweak/, new/)
# use the first 7 characters of the branch name as version
CORE_VERSION="$(printf '%s' "$CORE_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)"
fi
fi
# Gather web version information...
WEB_VERSION="$(GetPADDValue version.web.local.version)"
if [ ! "$WEB_VERSION" = "null" ]; then
WEB_BRANCH="$(GetPADDValue version.web.local.branch)"
WEB_VERSION="$(GetPADDValue version.web.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')"
GITHUB_WEB_VERSION="$(GetPADDValue version.web.remote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')"
WEB_HASH="$(GetPADDValue version.web.local.hash)"
GITHUB_WEB_HASH="$(GetPADDValue version.web.remote.hash)"
if [ "${WEB_BRANCH}" = "master" ]; then
web_version_converted="$(VersionConverter "${WEB_VERSION}")"
web_version_latest_converted=$(VersionConverter "${GITHUB_WEB_VERSION}")
if [ "${web_version_converted}" -lt "${web_version_latest_converted}" ]; then
out_of_date_flag="true"
web_version_heatmap=${red_text}
else
web_version_heatmap=${green_text}
fi
else
# Custom branch
if [ -z "${WEB_BRANCH}" ]; then
# Branch name is empty, something went wrong
web_version_heatmap=${red_text}
WEB_VERSION="?"
else
if [ "${WEB_HASH}" = "${GITHUB_WEB_HASH}" ]; then
# up-to-date
web_version_heatmap=${green_text}
else
# out-of-date
out_of_date_flag="true"
web_version_heatmap=${red_text}
fi
# shorten common branch names (fix/, tweak/, new/)
# use the first 7 characters of the branch name as version
WEB_VERSION="$(printf '%s' "$WEB_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)"
fi
fi
else
# Web interface not installed
WEB_VERSION="N/A"
web_version_heatmap=${yellow_text}
fi
# Gather FTL version information...
FTL_BRANCH="$(GetPADDValue version.ftl.local.branch)"
FTL_VERSION="$(GetPADDValue version.ftl.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')"
GITHUB_FTL_VERSION="$(GetPADDValue version.ftl.remote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')"
FTL_HASH="$(GetPADDValue version.ftl.local.hash)"
GITHUB_FTL_HASH="$(GetPADDValue version.ftl.remote.hash)"
if [ "${FTL_BRANCH}" = "master" ]; then
ftl_version_converted="$(VersionConverter "${FTL_VERSION}")"
ftl_version_latest_converted=$(VersionConverter "${GITHUB_FTL_VERSION}")
if [ "${ftl_version_converted}" -lt "${ftl_version_latest_converted}" ]; then
out_of_date_flag="true"
ftl_version_heatmap=${red_text}
else
ftl_version_heatmap=${green_text}
fi
else
# Custom branch
if [ -z "${FTL_BRANCH}" ]; then
# Branch name is empty, something went wrong
ftl_version_heatmap=${red_text}
FTL_VERSION="?"
else
if [ "${FTL_HASH}" = "${GITHUB_FTL_HASH}" ]; then
# up-to-date
ftl_version_heatmap=${green_text}
else
# out-of-date
out_of_date_flag="true"
ftl_version_heatmap=${red_text}
fi
# shorten common branch names (fix/, tweak/, new/)
# use the first 7 characters of the branch name as version
FTL_VERSION="$(printf '%s' "$FTL_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)"
fi
fi
}
GetPADDInformation() {
# PADD version information...
padd_version_latest="$(curl --connect-timeout 5 --silent https://api.github.com/repos/pi-hole/PADD/releases/latest | grep '"tag_name":' | awk -F \" '{print $4}')"
# is PADD up-to-date?
padd_out_of_date_flag=false
if [ -z "${padd_version_latest}" ]; then
padd_version_heatmap=${yellow_text}
else
padd_version_latest_converted="$(VersionConverter "${padd_version_latest}")"
padd_version_converted=$(VersionConverter "${padd_version}")
if [ "${padd_version_converted}" -lt "${padd_version_latest_converted}" ]; then
padd_out_of_date_flag="true"
padd_version_heatmap=${red_text}
else
# local and remote PADD version match or local is newer
padd_version_heatmap=${green_text}
fi
fi
}
GenerateSizeDependendOutput() {
if [ "$1" = "pico" ] || [ "$1" = "nano" ]; then
ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 9 "color")
elif [ "$1" = "micro" ]; then
ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 10 "color")
elif [ "$1" = "mini" ]; then
ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 20 "color")
latest_blocked=$(truncateString "$latest_blocked_raw" 29)
top_blocked=$(truncateString "$top_blocked_raw" 29)
elif [ "$1" = "tiny" ]; then
ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 30 "color")
latest_blocked=$(truncateString "$latest_blocked_raw" 41)
top_blocked=$(truncateString "$top_blocked_raw" 41)
top_domain=$(truncateString "$top_domain_raw" 41)
top_client=$(truncateString "$top_client_raw" 41)
elif [ "$1" = "regular" ] || [ "$1" = "slim" ]; then
ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 40 "color")
latest_blocked=$(truncateString "$latest_blocked_raw" 48)
top_blocked=$(truncateString "$top_blocked_raw" 48)
top_domain=$(truncateString "$top_domain_raw" 48)
top_client=$(truncateString "$top_client_raw" 48)
if [ "$temp_unicode" = true ]; then
temp_padding=21
else
temp_padding=20
fi
elif [ "$1" = "mega" ]; then
ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 30 "color")
latest_blocked=$(truncateString "$latest_blocked_raw" 68)
top_blocked=$(truncateString "$top_blocked_raw" 68)
top_domain=$(truncateString "$top_domain_raw" 68)
top_client=$(truncateString "$top_client_raw" 68)
if [ "$temp_unicode" = true ]; then
temp_padding=10
else
temp_padding=9
fi
fi
# System uptime
if [ "$1" = "pico" ] || [ "$1" = "nano" ] || [ "$1" = "micro" ]; then
system_uptime="$(convertUptime "${system_uptime_raw}" | awk -F ',' '{print $1 "," $2}')"
else
system_uptime="$(convertUptime "${system_uptime_raw}")"
fi
# Bar generations
if [ "$1" = "mini" ]; then
cpu_bar=$(BarGenerator "${cpu_percent}" 20)
memory_bar=$(BarGenerator "${memory_percent}" 20)
elif [ "$1" = "tiny" ]; then
cpu_bar=$(BarGenerator "${cpu_percent}" 7)
memory_bar=$(BarGenerator "${memory_percent}" 7)
else
cpu_bar=$(BarGenerator "${cpu_percent}" 10)
memory_bar=$(BarGenerator "${memory_percent}" 10)
fi
}
SetStatusMessage() {
# depending on which flags are set, the "message field" shows a different output
# 7 messages are possible (from highest to lowest priority):
# - System is hot
# - FTLDNS service is not running
# - Pi-hole's DNS server is off (FTL running, but not providing DNS)
# - Unable to determine Pi-hole blocking status
# - Pi-hole blocking disabled
# - Updates are available
# - Everything is fine
if [ "${hot_flag}" = true ]; then
# Check if CPU temperature is high
pico_status="${pico_status_hot}"
mini_status="${mini_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}"
tiny_status="${tiny_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}"
full_status="${full_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}"
mega_status="${mega_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}"
elif [ "${connection_down_flag}" = true ]; then
# Check if FTL is down
pico_status=${pico_status_ftl_down}
mini_status=${mini_status_ftl_down}
tiny_status=${tiny_status_ftl_down}
full_status=${full_status_ftl_down}
mega_status=${mega_status_ftl_down}
elif [ "${dns_down_flag}" = true ]; then
# Check if DNS is down
pico_status=${pico_status_dns_down}
mini_status=${mini_status_dns_down}
tiny_status=${tiny_status_dns_down}
full_status=${full_status_dns_down}
mega_status=${mega_status_dns_down}
elif [ "${blocking_enabled}" = "disabled" ]; then
# Check if blocking status is disabled
pico_status=${pico_status_off}
mini_status=${mini_status_off}
tiny_status=${tiny_status_off}
full_status=${full_status_off}
mega_status=${mega_status_off}
elif [ "${out_of_date_flag}" = "true" ] || [ "${padd_out_of_date_flag}" = "true" ]; then
# Check if one of the components of Pi-hole (or PADD itself) is out of date
pico_status=${pico_status_update}
mini_status=${mini_status_update}
tiny_status=${tiny_status_update}
full_status=${full_status_update}
mega_status=${mega_status_update}
elif [ "${blocking_enabled}" = "enabled" ]; then
# if we reach this point and blocking is enabled, everything is fine
pico_status=${pico_status_ok}
mini_status=${mini_status_ok}
tiny_status=${tiny_status_ok}
full_status=${full_status_ok}
mega_status=${mega_status_ok}
fi
}
############################################# PRINTERS #############################################
PrintLogo() {
if [ ! "${DOCKER_VERSION}" = "null" ]; then
version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}"
else
version_info="Pi-hole® ${core_version_heatmap}${CORE_VERSION}${reset_text}, Web ${web_version_heatmap}${WEB_VERSION}${reset_text}, FTL ${ftl_version_heatmap}${FTL_VERSION}${reset_text}"
fi
# Screen size checks
if [ "$1" = "pico" ]; then
printf "%s${clear_line}\n" "p${padd_text} ${pico_status}"
elif [ "$1" = "nano" ]; then
printf "%s${clear_line}\n" "n${padd_text} ${mini_status}"
elif [ "$1" = "micro" ]; then
printf "%s${clear_line}\n${clear_line}\n" "µ${padd_text} ${mini_status}"
elif [ "$1" = "mini" ]; then
printf "%s${clear_line}\n${clear_line}\n" "${padd_text}${dim_text}mini${reset_text} ${mini_status}"
elif [ "$1" = "tiny" ]; then
printf "%s${clear_line}\n" "${padd_text}${dim_text}tiny${reset_text} ${version_info}${reset_text}"
printf "%s${clear_line}\n" " PADD ${padd_version_heatmap}${padd_version}${reset_text} ${tiny_status}${reset_text}"
elif [ "$1" = "slim" ]; then
printf "%s${clear_line}\n${clear_line}\n" "${padd_text}${dim_text}slim${reset_text} ${full_status}"
elif [ "$1" = "regular" ] || [ "$1" = "slim" ]; then
printf "%s${clear_line}\n" "${padd_logo_1}"
printf "%s${clear_line}\n" "${padd_logo_2}${version_info}${reset_text}"
printf "%s${clear_line}\n${clear_line}\n" "${padd_logo_3}PADD ${padd_version_heatmap}${padd_version}${reset_text} ${full_status}${reset_text}"
# normal or not defined
else
printf "%s${clear_line}\n" "${padd_logo_retro_1}"
printf "%s${clear_line}\n" "${padd_logo_retro_2} ${version_info}, PADD ${padd_version_heatmap}${padd_version}${reset_text}"
printf "%s${clear_line}\n${clear_line}\n" "${padd_logo_retro_3} ${dns_check_box} DNS ${ftl_check_box} FTL ${mega_status}${reset_text}"
fi
}
PrintDashboard() {
if [ ! "${DOCKER_VERSION}" = "null" ]; then
version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}"
else
version_info="Pi-hole® ${core_version_heatmap}${CORE_VERSION}${reset_text}, Web ${web_version_heatmap}${WEB_VERSION}${reset_text}, FTL ${ftl_version_heatmap}${FTL_VERSION}${reset_text}"
fi
# Move cursor to (0,0).
printf '\e[H'
# adds the y-offset
moveYOffset
if [ "$1" = "pico" ]; then
# pico is a screen at least 20x10 (columns x lines)
moveXOffset; printf "%s${clear_line}\n" "p${padd_text} ${pico_status}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ============${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " [${ads_blocked_bar}] ${ads_percentage_today}%"
moveXOffset; printf "%s${clear_line}\n" " ${ads_blocked_today} / ${dns_queries_today}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ============${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " Hst: ${pi_hostname}"
moveXOffset; printf "%s${clear_line}\n" " IP: ${pi_ip4_addr}"
moveXOffset; printf "%s${clear_line}\n" " IPv6 ${ipv6_check_box} DHCP ${dhcp_check_box}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}CPU ================${reset_text}"
moveXOffset; printf "%s${clear_line}" " [${cpu_load_1_heatmap}${cpu_bar}${reset_text}] ${cpu_percent}%"
elif [ "$1" = "nano" ]; then
# nano is a screen at least 24x12 (columns x lines)
moveXOffset; printf "%s${clear_line}\n" "n${padd_text} ${mini_status}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ================${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " DNS: ${dns_check_box} FTL: ${ftl_check_box}"
moveXOffset; printf "%s${clear_line}\n" " Blk: [${ads_blocked_bar}] ${ads_percentage_today}%"
moveXOffset; printf "%s${clear_line}\n" " Blk: ${ads_blocked_today} / ${dns_queries_today}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ================${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " Host: ${pi_hostname}"
moveXOffset; printf "%s${clear_line}\n" " IP: ${pi_ip4_addr}"
moveXOffset; printf "%s${clear_line}\n" " IPv6: ${ipv6_check_box} DHCP: ${dhcp_check_box}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =================${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " Up: ${system_uptime}"
moveXOffset; printf "%s${clear_line}" " CPU: [${cpu_load_1_heatmap}${cpu_bar}${reset_text}] ${cpu_percent}%"
elif [ "$1" = "micro" ]; then
# micro is a screen at least 30x16 (columns x lines)
moveXOffset; printf "%s${clear_line}\n" "µ${padd_text} ${mini_status}"
moveXOffset; printf "%s${clear_line}\n" ""
moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ======================${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " DNS: ${dns_check_box} FTL: ${ftl_check_box}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ========================${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " Blckng: ${domains_being_blocked} domains"
moveXOffset; printf "%s${clear_line}\n" " Piholed: [${ads_blocked_bar}] ${ads_percentage_today}%"
moveXOffset; printf "%s${clear_line}\n" " Piholed: ${ads_blocked_today} / ${dns_queries_today}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ======================${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " Host: ${full_hostname}"
moveXOffset; printf "%s${clear_line}\n" " IP: ${pi_ip4_addr}"
moveXOffset; printf "%s${clear_line}\n" " IPv6: ${ipv6_check_box} DHCP: ${dhcp_check_box}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =======================${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " Uptime: ${system_uptime}"
moveXOffset; printf "%s${clear_line}\n" " Load: [${cpu_load_1_heatmap}${cpu_bar}${reset_text}] ${cpu_percent}%"
moveXOffset; printf "%s${clear_line}" " Memory: [${memory_heatmap}${memory_bar}${reset_text}] ${memory_percent}%"
elif [ "$1" = "mini" ]; then
# mini is a screen at least 40x18 (columns x lines)
moveXOffset; printf "%s${clear_line}\n" "${padd_text}${dim_text}mini${reset_text} ${mini_status}"
moveXOffset; printf "%s${clear_line}\n" ""
moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ================================${reset_text}"
moveXOffset; printf " %-9s${dns_heatmap}%-10s${reset_text} %-5s${ftl_heatmap}%-10s${reset_text}${clear_line}\n" "DNS:" "${dns_status}" "FTL:" "${ftl_status}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ==================================${reset_text}"
moveXOffset; printf " %-9s%-29s${clear_line}\n" "Blckng:" "${domains_being_blocked} domains"
moveXOffset; printf " %-9s[%-20s] %-5s${clear_line}\n" "Piholed:" "${ads_blocked_bar}" "${ads_percentage_today}%"
moveXOffset; printf " %-9s%-29s${clear_line}\n" "Piholed:" "${ads_blocked_today} out of ${dns_queries_today}"
moveXOffset; printf " %-9s%-29s${clear_line}\n" "Latest:" "${latest_blocked}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ================================${reset_text}"
moveXOffset; printf " %-9s%-16s%-5s%-9s${clear_line}\n" "Host:" "${full_hostname}" "DNS:" "${dns_information}"
moveXOffset; printf " %-9s%s${clear_line}\n" "IP:" "${pi_ip4_addr} (${iface_name})"
moveXOffset; printf " %-9s${ipv6_heatmap}%-10s${reset_text} %-8s${dhcp_heatmap}%-10s${reset_text}${clear_line}\n" "IPv6:" "${ipv6_status}" "DHCP:" "${dhcp_status}"
moveXOffset; printf " %-9s%-4s%-12s%-4s%-5s${clear_line}\n" "Traffic:" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =================================${reset_text}"
moveXOffset; printf " %-9s%-29s${clear_line}\n" "Uptime:" "${system_uptime}"
moveXOffset; printf "%s${clear_line}\n" " Load: [${cpu_load_1_heatmap}${cpu_bar}${reset_text}] ${cpu_percent}%"
moveXOffset; printf "%s${clear_line}" " Memory: [${memory_heatmap}${memory_bar}${reset_text}] ${memory_percent}%"
elif [ "$1" = "tiny" ]; then
# tiny is a screen at least 53x20 (columns x lines)
moveXOffset; printf "%s${clear_line}\n" "${padd_text}${dim_text}tiny${reset_text} ${version_info}${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " PADD ${padd_version_heatmap}${padd_version}${reset_text} ${tiny_status}${reset_text}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE =============================================${reset_text}"
moveXOffset; printf " %-10s${dns_heatmap}%-16s${reset_text} %-8s${ftl_heatmap}%-10s${reset_text}${clear_line}\n" "DNS:" "${dns_status}" "FTL:" "${ftl_status}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ===============================================${reset_text}"
moveXOffset; printf " %-10s%-29s${clear_line}\n" "Blocking:" "${domains_being_blocked} domains"
moveXOffset; printf " %-10s[%-30s] %-5s${clear_line}\n" "Pi-holed:" "${ads_blocked_bar}" "${ads_percentage_today}%"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Pi-holed:" "${ads_blocked_today} out of ${dns_queries_today}"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Latest:" "${latest_blocked}"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Ad:" "${top_blocked}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK =============================================${reset_text}"
moveXOffset; printf " %-10s%-16s %-8s%-16s${clear_line}\n" "Hostname:" "${full_hostname}" "IP: " "${pi_ip4_addr}"
moveXOffset; printf " %-10s%-16s %-4s%-7s %-4s%-5s${clear_line}\n" "Interfce:" "${iface_name}" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}"
moveXOffset; printf " %-10s%-16s %-8s${dnssec_heatmap}%-16s${reset_text}${clear_line}\n" "DNS:" "${dns_information}" "DNSSEC:" "${dnssec_status}"
moveXOffset; printf " %-10s%s${clear_line}\n" "IPv6:" "${pi_ip6_addr}"
moveXOffset; printf " %-10s%-5s %-4s${dhcp_range_heatmap}%-33s${reset_text}${clear_line}\n" "DHCP:" "${dhcp_check_box}" "Rng" "${dhcp_range}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM ==============================================${reset_text}"
moveXOffset; printf " %-10s%-29s${clear_line}\n" "Uptime:" "${system_uptime}"
moveXOffset; printf " %-10s${temp_heatmap}%-17s${reset_text} %-8s${cpu_load_1_heatmap}%-4s${reset_text}, ${cpu_load_5_heatmap}%-4s${reset_text}, ${cpu_load_15_heatmap}%-4s${reset_text}${clear_line}\n" "CPU Temp:" "${temperature}" "Load:" "${cpu_load_1}" "${cpu_load_5}" "${cpu_load_15}"
moveXOffset; printf " %-10s[${memory_heatmap}%-7s${reset_text}] %-6s %-8s[${cpu_load_1_heatmap}%-7s${reset_text}] %-5s${clear_line}" "Memory:" "${memory_bar}" "${memory_percent}%" "CPU:" "${cpu_bar}" "${cpu_percent}%"
elif [ "$1" = "regular" ] || [ "$1" = "slim" ]; then
# slim is a screen with at least 60 columns and exactly 21 lines
# regular is a screen at least 60x22 (columns x lines)
if [ "$1" = "slim" ]; then
moveXOffset; printf "%s${clear_line}\n" "${padd_text}${dim_text}slim${reset_text} ${version_info}${reset_text}"
moveXOffset; printf "%s${clear_line}\n" " PADD ${padd_version_heatmap}${padd_version}${reset_text} ${full_status}${reset_text}"
moveXOffset; printf "%s${clear_line}\n" ""
else
moveXOffset; printf "%s${clear_line}\n" "${padd_logo_1}"
moveXOffset; printf "%s${clear_line}\n" "${padd_logo_2}${version_info}${reset_text}"
moveXOffset; printf "%s${clear_line}\n" "${padd_logo_3}PADD ${padd_version_heatmap}${padd_version}${reset_text} ${full_status}${reset_text}"
moveXOffset; printf "%s${clear_line}\n" ""
fi
moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ====================================================${reset_text}"
moveXOffset; printf " %-10s${dns_heatmap}%-19s${reset_text} %-10s${ftl_heatmap}%-19s${reset_text}${clear_line}\n" "DNS:" "${dns_status}" "FTL:" "${ftl_status}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ======================================================${reset_text}"
moveXOffset; printf " %-10s%-49s${clear_line}\n" "Blocking:" "${domains_being_blocked} domains"
moveXOffset; printf " %-10s[%-40s] %-5s${clear_line}\n" "Pi-holed:" "${ads_blocked_bar}" "${ads_percentage_today}%"
moveXOffset; printf " %-10s%-49s${clear_line}\n" "Pi-holed:" "${ads_blocked_today} out of ${dns_queries_today} queries"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Latest:" "${latest_blocked}"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Ad:" "${top_blocked}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ====================================================${reset_text}"
moveXOffset; printf " %-10s%-15s %-4s%-17s${clear_line}\n" "Hostname:" "${full_hostname}" "IP:" "${pi_ip4_addr}"
moveXOffset; printf " %-10s%-15s %-4s%-17s%-4s%s${clear_line}\n" "Interfce:" "${iface_name}" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}"
moveXOffset; printf " %-10s%s${clear_line}\n" "IPv6:" "${pi_ip6_addr}"
moveXOffset; printf " %-10s%-15s %-10s${dnssec_heatmap}%-19s${reset_text}${clear_line}\n" "DNS:" "${dns_information}" "DNSSEC:" "${dnssec_status}"
moveXOffset; printf " %-10s%-16s%-6s${dhcp_range_heatmap}%-36s${reset_text}${clear_line}\n" "DHCP:" "${dhcp_check_box}" "Range" "${dhcp_range}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =====================================================${reset_text}"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Uptime:" "${system_uptime}"
moveXOffset; printf " %-10s${temp_heatmap}%-""${temp_padding}""s${reset_text}%-10s${cpu_load_1_heatmap}%-4s${reset_text}, ${cpu_load_5_heatmap}%-4s${reset_text}, ${cpu_load_15_heatmap}%-4s${reset_text}${clear_line}\n" "CPU Temp:" "${temperature}" "CPU Load:" "${cpu_load_1}" "${cpu_load_5}" "${cpu_load_15}"
moveXOffset; printf " %-10s[${memory_heatmap}%-10s${reset_text}] %-6s %-10s[${cpu_load_1_heatmap}%-10s${reset_text}] %-5s${clear_line}" "Memory:" "${memory_bar}" "${memory_percent}%" "CPU Load:" "${cpu_bar}" "${cpu_percent}%"
else # ${padd_size} = mega
# mega is a screen with at least 80 columns and 26 lines
moveXOffset; printf "%s${clear_line}\n" "${padd_logo_retro_1}"
moveXOffset; printf "%s${clear_line}\n" "${padd_logo_retro_2} ${version_info}, PADD ${padd_version_heatmap}${padd_version}${reset_text}"
moveXOffset; printf "%s${clear_line}\n" "${padd_logo_retro_3} ${dns_check_box} DNS ${ftl_check_box} FTL ${mega_status}${reset_text}"
moveXOffset; printf "%s${clear_line}\n" ""
moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ==========================================================================${reset_text}"
moveXOffset; printf " %-10s%-19s %-10s[%-40s] %-5s${clear_line}\n" "Blocking:" "${domains_being_blocked} domains" "Piholed:" "${ads_blocked_bar}" "${ads_percentage_today}%"
moveXOffset; printf " %-10s%-30s%-29s${clear_line}\n" "Clients:" "${clients}" " ${ads_blocked_today} out of ${dns_queries_today} queries"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Latest:" "${latest_blocked}"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Ad:" "${top_blocked}"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Dmn:" "${top_domain}"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Clnt:" "${top_client}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}FTL ============================================================================${reset_text}"
moveXOffset; printf " %-10s%-9s %-10s%-9s %-10s%-9s${clear_line}\n" "PID:" "${ftlPID}" "CPU Use:" "${ftl_cpu}" "Mem. Use:" "${ftl_mem_percentage}"
moveXOffset; printf " %-10s%-69s${clear_line}\n" "DNSCache:" "${cache_inserts} insertions, ${cache_evictions} deletions, ${cache_size} total entries"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ========================================================================${reset_text}"
moveXOffset; printf " %-10s%-19s${clear_line}\n" "Hostname:" "${full_hostname}"
moveXOffset; printf " %-10s%-15s %-4s%-9s %-4s%-9s${clear_line}\n" "Interfce:" "${iface_name}" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}"
moveXOffset; printf " %-6s%-19s %-10s%-29s${clear_line}\n" "IPv4:" "${pi_ip4_addr}" "IPv6:" "${pi_ip6_addr}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}DNS ==========================DHCP==============================================${reset_text}"
moveXOffset; printf " %-10s%-19s %-6s${dhcp_heatmap}%-19s${reset_text}${clear_line}\n" "Servers:" "${dns_information}" "DHCP:" "${dhcp_status}"
moveXOffset; printf " %-10s${dnssec_heatmap}%-19s${reset_text} %-10s${dhcp_ipv6_heatmap}%-9s${reset_text}${clear_line}\n" "DNSSEC:" "${dnssec_status}" "IPv6 Spt:" "${dhcp_ipv6_status}"
moveXOffset; printf " %-10s${conditional_forwarding_heatmap}%-20s${reset_text}%-6s${dhcp_range_heatmap}%-36s${reset_text}${clear_line}\n" "CdFwding:" "${conditional_forwarding_status}" "Range" "${dhcp_range}"
moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =========================================================================${reset_text}"
moveXOffset; printf " %-10s%-39s${clear_line}\n" "Device:" "${sys_model}"
moveXOffset; printf " %-10s%-39s %-10s[${memory_heatmap}%-10s${reset_text}] %-6s${clear_line}\n" "Uptime:" "${system_uptime}" "Memory:" "${memory_bar}" "${memory_percent}%"
moveXOffset; printf " %-10s${temp_heatmap}%-""${temp_padding}""s${reset_text} %-10s${cpu_load_1_heatmap}%-4s${reset_text}, ${cpu_load_5_heatmap}%-4s${reset_text}, ${cpu_load_15_heatmap}%-7s${reset_text} %-10s[${memory_heatmap}%-10s${reset_text}] %-6s${clear_line}" "CPU Temp:" "${temperature}" "CPU Load:" "${cpu_load_1}" "${cpu_load_5}" "${cpu_load_15}" "CPU Load:" "${cpu_bar}" "${cpu_percent}%"
fi
# Clear to end of screen (below the drawn dashboard)
# https://vt100.net/docs/vt510-rm/ED.html
printf '\e[0J'
}
############################################# HELPERS ##############################################
# Provides a color based on a provided percentage
# takes in one or two parameters
HeatmapGenerator () {
# if one number is provided, just use that percentage to figure out the colors
if [ -z "$2" ]; then
load=$(printf "%.0f" "$1")
# if two numbers are provided, do some math to make a percentage to figure out the colors
else
load=$(printf "%.0f" "$(echo "$1 $2" | awk '{print ($1 / $2) * 100}')")
fi
# Color logic
# |<- green ->| yellow | red ->
# 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100
if [ "${load}" -lt 75 ]; then
out=${green_text}
elif [ "${load}" -lt 90 ]; then
out=${yellow_text}
else
out=${red_text}
fi
echo "$out"
}
# Provides a "bar graph"
# takes in two or three parameters
# $1: percentage filled
# $2: max length of the bar
# $3: colored flag, if "color" backfill with color
BarGenerator() {
# number of filled in cells in the bar
barNumber=$(printf %.f "$(echo "$1 $2" | awk '{print ($1 / 100) * $2}')")
frontFill=$(for i in $(seq "$barNumber"); do printf "%b" "■"; done)
# remaining "unfilled" cells in the bar
backfillNumber=$(($2-barNumber))
# if the filled in cells is less than the max length of the bar, fill it
if [ "$barNumber" -lt "$2" ]; then
# if the bar should be colored
if [ "$3" = "color" ]; then
# fill the rest in color
backFill=$(for i in $(seq $backfillNumber); do printf "%b" "■"; done)
out="${red_text}${frontFill}${green_text}${backFill}${reset_text}"
# else, it shouldn't be colored in
else
# fill the rest with "space"
backFill=$(for i in $(seq $backfillNumber); do printf "%b" "·"; done)
out="${frontFill}${reset_text}${backFill}"
fi
# else, fill it all the way
else
out=$(for i in $(seq "$2"); do printf "%b" "■"; done)
fi
echo "$out"
}
# Checks the size of the screen and sets the value of ${padd_data}_size
SizeChecker(){
# adding a tiny delay here to to give the kernel a bit time to
# report new sizes correctly after a terminal resize
# this reduces "flickering" of GenerateSizeDependendOutput() items
# after a terminal re-size
sleep 0.1
console_width=$(tput cols)
console_height=$(tput lines)
# Mega
if [ "$console_width" -ge "80" ] && [ "$console_height" -ge "26" ]; then
padd_size="mega"
width=80
height=26
# Below Mega. Gives you Regular.
elif [ "$console_width" -ge "60" ] && [ "$console_height" -ge "22" ]; then
padd_size="regular"
width=60
height=22
# Below Regular. Gives you Slim.
elif [ "$console_width" -ge "60" ] && [ "$console_height" -ge "21" ]; then
padd_size="slim"
width=60
height=21
# Below Slim. Gives you Tiny.
elif [ "$console_width" -ge "53" ] && [ "$console_height" -ge "20" ]; then
padd_size="tiny"
width=53
height=20
# Below Tiny. Gives you Mini.
elif [ "$console_width" -ge "40" ] && [ "$console_height" -ge "18" ]; then
padd_size="mini"
width=40
height=18
# Below Mini. Gives you Micro.
elif [ "$console_width" -ge "30" ] && [ "$console_height" -ge "16" ]; then
padd_size="micro"
width=30
height=16
# Below Micro, Gives you Nano.
elif [ "$console_width" -ge "24" ] && [ "$console_height" -ge "12" ]; then
padd_size="nano"
width=24
height=12
# Below Nano. Gives you Pico.
elif [ "$console_width" -ge "20" ] && [ "$console_height" -ge "10" ]; then
padd_size="pico"
width=20
height=10
# Below Pico. Gives you nothing...
else
padd_size="ants"
fi
# Center the output (default position)
xOffset="$(( (console_width - width) / 2 ))"
yOffset="$(( (console_height - height) / 2 ))"
# If the user sets an offset option, use it.
if [ -n "$xOffOrig" ]; then
xOffset=$xOffOrig
# Limit the offset to avoid breaks
xMaxOffset=$((console_width - width))
if [ "$xOffset" -gt "$xMaxOffset" ]; then
xOffset="$xMaxOffset"
fi
fi
if [ -n "$yOffOrig" ]; then
yOffset=$yOffOrig
# Limit the offset to avoid breaks
yMaxOffset=$((console_height - height))
if [ "$yOffset" -gt "$yMaxOffset" ]; then
yOffset="$yMaxOffset"
fi
fi
}
# converts a given version string e.g. v3.7.1 to 3007001000 to allow for easier comparison of multi digit version numbers
# credits https://apple.stackexchange.com/a/123408
VersionConverter() {
echo "$@" | tr -d '[:alpha:]' | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }';
}
moveYOffset(){
# moves the cursor yOffset-times down
# https://vt100.net/docs/vt510-rm/CUD.html
# this needs to be guarded, because if the amount is 0, it is adjusted to 1
# https://terminalguide.namepad.de/seq/csi_cb/
if [ "${yOffset}" -gt 0 ]; then
printf '\e[%sB' "${yOffset}"
fi
}
moveXOffset(){
# moves the cursor xOffset-times to the right
# https://vt100.net/docs/vt510-rm/CUF.html
# this needs to be guarded, because if the amount is 0, it is adjusted to 1
# https://terminalguide.namepad.de/seq/csi_cb/
if [ "${xOffset}" -gt 0 ]; then
printf '\e[%sC' "${xOffset}"
fi
}
# Remove undesired strings from sys_model variable - used in GetSystemInformation() function
filterModel() {
FILTERLIST="To be filled by O.E.M.|Not Applicable|System Product Name|System Version|Undefined|Default string|Not Specified|Type1ProductConfigId|INVALID|All Series|<7C>"
# Description:
# `-v` : set $FILTERLIST into a variable called `list`
# `gsub()` : replace all list items (ignoring case) with an empty string, deleting them
# `{$1=$1}1`: remove all extra spaces. The last "1" evaluates as true, printing the result
echo "$1" | awk -v list="$FILTERLIST" '{IGNORECASE=1; gsub(list,"")}; {$1=$1}1'
}
# Truncates a given string and appends three '...'
# takes two parameters
# $1: string to truncate
# $2: max length of the string
truncateString() {
local truncatedString length shorted
length=${#1}
shorted=$(($2-3)) # shorten max allowed length by 3 to make room for the dots
if [ "${length}" -gt "$2" ]; then
# if length of the string is larger then the specified max length
# cut every char from the string exceeding length $shorted and add three dots
truncatedString=$(echo "$1" | cut -c1-$shorted)"..."
echo "${truncatedString}"
else
echo "$1"
fi
}
# Converts seconds to days, hours, minutes
# https://unix.stackexchange.com/a/338844
convertUptime() {
local D=$(($1/60/60/24))
local H=$(($1/60/60%24))
local M=$(($1/60%60))
printf "%d days, %02d hours, %02d minutes" $D $H $M
}
secretRead() {
# POSIX compliant function to read user-input and
# mask every character entered by (*)
#
# This is challenging, because in POSIX, `read` does not support
# `-s` option (suppressing the input) or
# `-n` option (reading n chars)
# This workaround changes the terminal characteristics to not echo input and later resets this option
# credits https://stackoverflow.com/a/4316765
# showing asterisk instead of password
# https://stackoverflow.com/a/24600839
# https://unix.stackexchange.com/a/464963
stty -echo # do not echo user input
stty -icanon min 1 time 0 # disable canonical mode https://man7.org/linux/man-pages/man3/termios.3.html
unset password
unset key
unset charcount
charcount=0
while key=$(dd ibs=1 count=1 2>/dev/null); do #read one byte of input
if [ "${key}" = "$(printf '\0' | tr -d '\0')" ] ; then
# Enter - accept password
break
fi
if [ "${key}" = "$(printf '\177')" ] ; then
# Backspace
if [ $charcount -gt 0 ] ; then
charcount=$((charcount-1))
printf '\b \b'
password="${password%?}"
fi
else
# any other character
charcount=$((charcount+1))
printf '*'
password="$password$key"
fi
done
# restore original terminal settings
stty "${stty_orig}"
}
check_dependencies() {
# Check for required dependencies
if ! command -v curl >/dev/null 2>&1; then
printf "%b" "${check_box_bad} Error!\n 'curl' is missing but required.\n"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
printf "%b" "${check_box_bad} Error!\n 'jq' is missing but required.\n"
exit 1
fi
}
########################################## MAIN FUNCTIONS ##########################################
ShowVersion() {
# Hiding the cursor.
# https://vt100.net/docs/vt510-rm/DECTCEM.html
printf '\e[?25l'
# Traps for graceful shutdown
# https://unix.stackexchange.com/a/681201
trap CleanExit EXIT
trap sig_cleanup INT QUIT TERM
# Save current terminal settings (needed for later restore after password prompt)
stty_orig=$(stty -g)
# Test if the authentication endpoint is available
TestAPIAvailability
# Authenticate with the FTL server
printf "%b" "Establishing connection with FTL...\n"
LoginAPI
GetPADDData
GetVersionInformation
GetPADDInformation
if [ -z "${padd_version_latest}" ]; then
padd_version_latest="N/A"
fi
if [ ! "${DOCKER_VERSION}" = "null" ]; then
# Check for latest Docker version
printf "\n%s${clear_line}\n" "PADD version is ${padd_version} as part of Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text} (Latest Docker: ${GITHUB_DOCKER_VERSION})"
version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}"
else
printf "\n%s${clear_line}\n" "PADD version is ${padd_version_heatmap}${padd_version}${reset_text} (Latest: ${padd_version_latest})"
fi
exit 0
}
StartupRoutine(){
if [ "$1" = "ants" ]; then
# If the screen is too small from the beginning, exit
printf "%b" "${check_box_bad} Error!\n PADD isn't\n for ants!\n"
exit 1
fi
# Clear the screen and move cursor to (0,0).
# This mimics the 'clear' command.
# https://vt100.net/docs/vt510-rm/ED.html
# https://vt100.net/docs/vt510-rm/CUP.html
# E3 extension `\e[3J` to clear the scrollback buffer see 'man clear'
printf '\e[H\e[2J\e[3J'
# adds the y-offset
moveYOffset
if [ "$1" = "pico" ] || [ "$1" = "nano" ] || [ "$1" = "micro" ]; then
moveXOffset; PrintLogo "$1"
moveXOffset; printf "%b" "START-UP ===========\n"
# Test if the authentication endpoint is available
TestAPIAvailability
# Authenticate with the FTL server
moveXOffset; printf "%b" "Establishing connection with FTL...\n"
LoginAPI
moveXOffset; printf "%b" "Starting PADD...\n"
moveXOffset; printf "%b" " [■·········] 10%\r"
# Request PADD data
GetPADDData
# Check for updates
moveXOffset; printf "%b" " [■■········] 20%\r"
moveXOffset; printf "%b" " [■■■·······] 30%\r"
# Get our information for the first time
moveXOffset; printf "%b" " [■■■■······] 40%\r"
GetVersionInformation
moveXOffset; printf "%b" " [■■■■■·····] 50%\r"
GetSummaryInformation
moveXOffset; printf "%b" " [■■■■■■····] 60%\r"
GetPiholeInformation
moveXOffset; printf "%b" " [■■■■■■■···] 70%\r"
GetNetworkInformation
moveXOffset; printf "%b" " [■■■■■■■■··] 80%\r"
GetSystemInformation
moveXOffset; printf "%b" " [■■■■■■■■■·] 90%\r"
GetPADDInformation
moveXOffset; printf "%b" " [■■■■■■■■■■] 100%\n"
elif [ "$1" = "mini" ]; then
moveXOffset; PrintLogo "$1"
moveXOffset; echo "START UP ====================="
# Test if the authentication endpoint is available
TestAPIAvailability
# Authenticate with the FTL server
moveXOffset; printf "%b" "Establishing connection with FTL...\n"
LoginAPI
# Request PADD data
moveXOffset; echo "- Requesting PADD information..."
GetPADDData
# Get our information for the first time
moveXOffset; echo "- Gathering version info."
GetVersionInformation
moveXOffset; echo "- Gathering system info."
GetSystemInformation
moveXOffset; echo "- Gathering CPU/DNS info."
GetPiholeInformation
GetSummaryInformation
moveXOffset; echo "- Gathering network info."
GetNetworkInformation
GetPADDInformation
if [ ! "${DOCKER_VERSION}" = "null" ]; then
moveXOffset; echo " - Docker Tag ${DOCKER_VERSION}"
else
moveXOffset; echo " - Core $CORE_VERSION, Web $WEB_VERSION"
moveXOffset; echo " - FTL $FTL_VERSION, PADD ${padd_version}"
fi
else
moveXOffset; printf "%b" "${padd_logo_retro_1}\n"
moveXOffset; printf "%b" "${padd_logo_retro_2}Pi-hole® Ad Detection Display\n"
moveXOffset; printf "%b" "${padd_logo_retro_3}A client for Pi-hole\n\n"
if [ "$1" = "tiny" ]; then
moveXOffset; echo "START UP ============================================"
else
moveXOffset; echo "START UP ==================================================="
fi
# Test if the authentication endpoint is available
TestAPIAvailability
# Authenticate with the FTL server
moveXOffset; printf "%b" "Establishing connection with FTL...\n"
LoginAPI
# Request PADD data
moveXOffset; echo "- Requesting PADD information..."
GetPADDData
# Get our information for the first time
moveXOffset; echo "- Gathering version information..."
GetVersionInformation
moveXOffset; echo "- Gathering system information..."
GetSystemInformation
moveXOffset; echo "- Gathering CPU/DNS information..."
GetSummaryInformation
GetPiholeInformation
moveXOffset; echo "- Gathering network information..."
GetNetworkInformation
GetPADDInformation
if [ ! "${DOCKER_VERSION}" = "null" ]; then
moveXOffset; echo " - Docker Tag ${DOCKER_VERSION}"
else
moveXOffset; echo " - Pi-hole Core $CORE_VERSION"
moveXOffset; echo " - Web Admin $WEB_VERSION"
moveXOffset; echo " - FTL $FTL_VERSION"
moveXOffset; echo " - PADD ${padd_version}"
fi
fi
moveXOffset; printf "%s" "- Starting in "
for i in 3 2 1
do
printf "%s..." "$i"
sleep 1
done
}
NormalPADD() {
# Trap the window resize signal (handle window resize events)
trap 'TerminalResize' WINCH
# Clear the screen once on startup to remove overflow from the startup routine
printf '\033[2J'
while true; do
# Generate output that depends on the terminal size
# e.g. Heatmap and barchart
GenerateSizeDependendOutput ${padd_size}
# Sets the message displayed in the "status field" depending on the set flags
SetStatusMessage
# Output everything to the screen
PrintDashboard ${padd_size}
# Sleep for 5 seconds
# sending sleep in the background and wait for it
# this way the TerminalResize trap can kill the sleep
# and force a instant re-draw of the dashboard
# https://stackoverflow.com/questions/32041674/linux-how-to-kill-sleep
#
# saving the PID of the background sleep process to kill it on exit and resize
sleep 5 &
sleepPID=$!
wait $!
# Start getting our information for next round
now=$(date +%s)
# check if a new authentication is required (e.g. after connection to FTL has re-established)
# GetFTLData() will return a 401 if a 401 http status code is returned
# as $password should be set already, PADD should automatically re-authenticate
authenthication_required=$(GetFTLData "info/ftl")
if [ "${authenthication_required}" = 401 ]; then
Authenticate
fi
# Request PADD data after 30 seconds or if the connection was lost
if [ $((now - LastCheckFullInformation)) -ge 30 ] || [ "${connection_down_flag}" = true ] ; then
GetPADDData
LastCheckFullInformation="${now}"
else
# Request only a subset of the data
GetPADDData "?full=false"
fi
connection_down_flag=false
# If the connection was lost, set connection_down_flag
if [ "${padd_data}" = "000" ]; then
connection_down_flag=true
GetSystemInformation
GetSummaryInformation
GetPiholeInformation
GetNetworkInformation
GetVersionInformation
# set flag to update network information in the next loop in case the connection is re-established
get_network_information_requried=true
else
# Get uptime, CPU load, temp, etc. every 5 seconds
GetSystemInformation
GetSummaryInformation
GetPiholeInformation
if [ $((now - LastCheckNetworkInformation)) -ge 30 ] || [ "${get_network_information_requried}" = true ]; then
GetNetworkInformation
GetVersionInformation
LastCheckNetworkInformation="${now}"
get_network_information_requried=false
fi
# Get PADD version information every 24hours
if [ $((now - LastCheckPADDInformation)) -ge 86400 ]; then
GetPADDInformation
LastCheckPADDInformation="${now}"
fi
fi
done
}
Update() {
GetPADDInformation
if [ "${padd_out_of_date_flag}" = "true" ]; then
echo "${check_box_info} Updating PADD from ${padd_version} to ${padd_version_latest}"
padd_script_path=$(realpath "$0")
echo "${check_box_info} Downloading PADD update ..."
if curl --connect-timeout 5 -sSL https://install.padd.sh -o "${padd_script_path}" > /dev/null 2>&1; then
echo "${check_box_good} ... done. Restart PADD for the update to take effect"
else
echo "${check_box_bad} Cannot download PADD update"
echo "${check_box_info} Go to https://install.padd.sh to download the update manually"
exit 1
fi
else
echo "${check_box_good} You are already using the latest PADD version ${padd_version}"
fi
exit 0
}
DisplayHelp() {
cat << EOM
::: PADD displays stats about your Pi-hole!
:::
:::
::: Options:
::: --xoff [num] set the x-offset, reference is the upper left corner, disables auto-centering
::: --yoff [num] set the y-offset, reference is the upper left corner, disables auto-centering
:::
::: --server <DOMAIN|IP> domain or IP of your Pi-hole (default: localhost)
::: --secret <password> your Pi-hole's password, required to access the API
::: --2fa <2fa> your Pi-hole's 2FA code, if 2FA is enabled
::: -u, --update update to the latest version
::: -v, --version show PADD version info
::: -h, --help display this help text
EOM
}
# Called on signals INT QUIT TERM
sig_cleanup() {
# save error code (130 for SIGINT, 143 for SIGTERM, 131 for SIGQUIT)
err=$?
# some shells will call EXIT after the INT signal
# causing EXIT trap to be executed, so we trap EXIT after INT
trap '' EXIT
(exit $err) # execute in a subshell just to pass $? to CleanExit()
CleanExit
}
# Called on signal EXIT, or indirectly on INT QUIT TERM
CleanExit() {
# save the return code of the script
err=$?
# reset trap for all signals to not interrupt clean_tempfiles() on any next signal
trap '' EXIT INT QUIT TERM
# restore terminal settings if they have been changed (e.g. user canceled script while at password input prompt)
if [ "$(stty -g)" != "${stty_orig}" ]; then
stty "${stty_orig}"
fi
# Show the cursor
# https://vt100.net/docs/vt510-rm/DECTCEM.html
printf '\e[?25h'
# if background sleep is running, kill it
# http://mywiki.wooledge.org/SignalTrap#When_is_the_signal_handled.3F
kill "{$sleepPID}" > /dev/null 2>&1
# Delete session from FTL server
DeleteSession
exit $err # exit the script with saved $?
}
TerminalResize(){
# if a terminal resize is trapped, check the new terminal size and
# kill the sleep function within NormalPADD() to trigger redrawing
# of the Dashboard
SizeChecker
# Clear the screen and move cursor to (0,0).
# This mimics the 'clear' command.
# https://vt100.net/docs/vt510-rm/ED.html
# https://vt100.net/docs/vt510-rm/CUP.html
# E3 extension `\e[3J` to clear the scrollback buffer (see 'man clear')
printf '\e[H\e[2J\e[3J'
kill "{$sleepPID}" > /dev/null 2>&1
}
main(){
check_dependencies
# Hiding the cursor.
# https://vt100.net/docs/vt510-rm/DECTCEM.html
printf '\e[?25l'
# Traps for graceful shutdown
# https://unix.stackexchange.com/a/681201
trap CleanExit EXIT
trap sig_cleanup INT QUIT TERM
# Save current terminal settings (needed for later restore after password prompt)
stty_orig=$(stty -g)
SizeChecker
StartupRoutine ${padd_size}
# Run SizeChecker again to account for resizing during startup
SizeChecker
# Run PADD
NormalPADD
}
# Process all options (if present)
while [ "$#" -gt 0 ]; do
case "$1" in
"-u" | "--update" ) xOffset=0; doUpdate=true;;
"-h" | "--help" ) DisplayHelp; exit 0;;
"-v" | "--version" ) xOffset=0; versionOnly=true ;;
"--xoff" ) xOffset="$2"; xOffOrig="$2"; shift;;
"--yoff" ) yOffset="$2"; yOffOrig="$2"; shift;;
"--server" ) SERVER="$2"; shift;;
"--secret" ) password="$2"; shift;;
"--2fa" ) totp="$2"; shift;;
* ) DisplayHelp; exit 1;;
esac
shift
done
if [ "${versionOnly}" ]; then
ShowVersion
fi
if [ "${doUpdate}" ]; then
Update
fi
main