#!/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 ]" 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) cpu_temp=$(printf "%.1f" "${cpu_temp_raw}") temp_unit=$(echo "${padd_data}" | GetPADDValue sensors.unit) # 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}')" 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}')" 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}')" else # unknown unit temperature="${cpu_temp}°?" # no conversion needed cpu_temp_celsius=0 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} else temp_heatmap=${cyan_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.remmote.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.remmote.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.remmote.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() { # If PADD is running inside docker, immediately return without checking for an update if [ ! "${DOCKER_VERSION}" = "null" ]; then return fi # 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) 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) 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%-15s%-4s${dhcp_range_heatmap}%-36s${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}%-21s${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}%-10s${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|�" # 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() { # shellcheck disable=SC2016 eval "echo $(date -ud "@$1" +'$((%s/3600/24)) days, %H hours, %M minutes')" } 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 ########################################## OutputJSON() { # 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 GetSummaryInformation printf "%b" "{\"domains_being_blocked\":${domains_being_blocked_raw},\"dns_queries_today\":${dns_queries_today_raw},\"ads_blocked_today\":${ads_blocked_today_raw},\"ads_percentage_today\":${ads_percentage_today},\"clients\": ${clients}}" } 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 "%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 "%s${clear_line}\n" "PADD version is ${padd_version_heatmap}${padd_version}${reset_text} (Latest: ${padd_version_latest})" fi } 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() { # source version file to check if $DOCKER_VERSION is set . /etc/pihole/versions if [ -n "${DOCKER_VERSION}" ]; then echo "${check_box_info} Update is not supported for Docker" exit 1 fi 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 or IP of your Pi-hole (default: localhost) ::: --secret your Pi-hole's password, required to access the API ::: --2fa <2fa> your Pi-hole's 2FA code, if 2FA is enabled ::: -j, --json output stats as JSON formatted string and exit ::: -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 PADD NormalPADD } # Process all options (if present) while [ "$#" -gt 0 ]; do case "$1" in "-j" | "--json" ) xOffset=0; OutputJSON; exit 0;; "-u" | "--update" ) Update;; "-h" | "--help" ) DisplayHelp; exit 0;; "-v" | "--version" ) xOffset=0; ShowVersion; exit 0;; "--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 main