├── .codespellignore ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── codespell.yml │ ├── editorconfig-checker.yml │ ├── merge-conflict.yml │ ├── stale.yml │ ├── stale_pr.yml │ ├── sync-back-to-dev.yml │ └── version_bump.yml ├── .gitignore ├── .yamllint.conf ├── README.md └── padd.sh /.codespellignore: -------------------------------------------------------------------------------- 1 | padd 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://editorconfig.org/ 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = tab 10 | tab_width = 4 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 13 | 1. ... 14 | 2. ... 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | day: saturday 8 | time: "10:00" 9 | open-pull-requests-limit: 10 10 | target-branch: development 11 | reviewers: 12 | - "pi-hole/padd-maintainers" 13 | -------------------------------------------------------------------------------- /.github/workflows/codespell.yml: -------------------------------------------------------------------------------- 1 | name: Codespell 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened, ready_for_review] 5 | 6 | jobs: 7 | spell-check: 8 | if: github.event.pull_request.draft == false 9 | runs-on: ubuntu-latest 10 | steps: 11 | - 12 | name: Checkout repository 13 | uses: actions/checkout@v4.2.2 14 | - 15 | name: Spell-Checking 16 | uses: codespell-project/actions-codespell@master 17 | with: 18 | ignore_words_file: .codespellignore 19 | -------------------------------------------------------------------------------- /.github/workflows/editorconfig-checker.yml: -------------------------------------------------------------------------------- 1 | name: editorconfig-checker 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | build: 9 | name: editorconfig-checker 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4.2.2 13 | - uses: editorconfig-checker/action-editorconfig-checker@main # current tag v1.0.0 is really out-of-date 14 | - run: editorconfig-checker 15 | -------------------------------------------------------------------------------- /.github/workflows/merge-conflict.yml: -------------------------------------------------------------------------------- 1 | name: "Check for merge conflicts" 2 | on: 3 | # So that PRs touching the same files as the push are updated 4 | push: 5 | # So that the `dirtyLabel` is removed if conflicts are resolve 6 | # We recommend `pull_request_target` so that github secrets are available. 7 | # In `pull_request` we wouldn't be able to change labels of fork PRs 8 | pull_request_target: 9 | types: [synchronize] 10 | 11 | jobs: 12 | main: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check if PRs are have merge conflicts 16 | uses: eps1lon/actions-label-merge-conflict@v3.0.3 17 | with: 18 | dirtyLabel: "Merge Conflict" 19 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 20 | commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request." 21 | commentOnClean: "Conflicts have been resolved." 22 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 8 * * *' 6 | workflow_dispatch: 7 | issue_comment: 8 | 9 | env: 10 | stale_label: stale 11 | 12 | jobs: 13 | stale_action: 14 | if: github.event_name != 'issue_comment' 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | 19 | steps: 20 | - uses: actions/stale@v9.1.0 21 | with: 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | days-before-stale: 30 24 | days-before-close: 5 25 | stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Please comment or update this issue or it will be closed in 5 days." 26 | stale-issue-label: '${{ env.stale_label }}' 27 | exempt-issue-labels: "Bug, WIP, Fixed In Next Release, Internal, Never Stale" 28 | exempt-all-issue-assignees: true 29 | operations-per-run: 300 30 | close-issue-reason: "not_planned" 31 | 32 | remove_stale: 33 | # trigger "stale" removal immediately when stale issues are commented on 34 | # we need to explicitly check that the trigger does not run on comment on a PR as 35 | # 'issue_comment' triggers on issues AND PR comments 36 | if: ${{ !github.event.issue.pull_request && github.event_name != 'schedule' }} 37 | permissions: 38 | contents: read # for actions/checkout 39 | issues: write # to edit issues label 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4.2.2 44 | - name: Remove 'stale' label 45 | run: gh issue edit ${{ github.event.issue.number }} --remove-label ${{ env.stale_label }} 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/stale_pr.yml: -------------------------------------------------------------------------------- 1 | name: Close stale PR 2 | # This action will add a `stale` label and close immediately every PR that meets the following conditions: 3 | # - it is already marked with "merge conflict" label 4 | # - there was no update/comment on the PR in the last 30 days. 5 | 6 | on: 7 | schedule: 8 | - cron: '0 10 * * *' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | stale: 13 | 14 | runs-on: ubuntu-latest 15 | permissions: 16 | issues: write 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/stale@v9.1.0 21 | with: 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | # Do not automatically mark PR/issue as stale 24 | days-before-stale: -1 25 | # Override 'days-before-stale' for PR only 26 | days-before-pr-stale: 30 27 | # Close PRs immediately, after marking them 'stale' 28 | days-before-pr-close: 0 29 | # only run the action on merge conflict PR 30 | any-of-labels: 'Merge Conflict' 31 | exempt-pr-labels: 'Internal, Never Stale, On Hold, WIP' 32 | exempt-all-pr-assignees: true 33 | operations-per-run: 300 34 | stale-pr-message: '' 35 | close-pr-message: 'Existing merge conflicts have not been addressed. This PR is considered abandoned.' 36 | -------------------------------------------------------------------------------- /.github/workflows/sync-back-to-dev.yml: -------------------------------------------------------------------------------- 1 | name: Sync Back to Development 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | sync-branches: 10 | runs-on: ubuntu-latest 11 | name: Syncing branches 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4.2.2 15 | - name: Opening pull request 16 | run: gh pr create -B development -H master --title 'Sync master back into development' --body 'Created by Github action' --label 'Internal' 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/version_bump.yml: -------------------------------------------------------------------------------- 1 | name: Version bump 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: "0 * * * *" 7 | 8 | jobs: 9 | bump: 10 | runs-on: ubuntu-latest 11 | env: 12 | REPO: ${{ github.repository }} 13 | GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 14 | steps: 15 | - name: Get latest release tag # can also be a draft release 16 | run: | 17 | LATEST_TAG=$(gh -R $REPO release list -L 1 | awk '{printf $3}') 18 | echo "Latest version tag for releases in $REPO is $LATEST_TAG" 19 | echo "latest_tag=$LATEST_TAG" >> $GITHUB_ENV 20 | # Exit when LATEST_TAG is empty, due to github connection errors 21 | if [[ -z "${LATEST_TAG}" ]]; then echo "Error: LATEST_TAG is empty, aborting" && exit 1; fi; 22 | shell: bash 23 | 24 | - name: Checkout code 25 | uses: actions/checkout@v4.2.2 26 | with: 27 | ref: 'development' 28 | 29 | - name: Check if branch already exists 30 | run: | 31 | LATEST_TAG=${{ env.latest_tag }} 32 | REMOTE_BRANCH=$(git ls-remote --heads origin "bump_$LATEST_TAG") 33 | [[ -z "${REMOTE_BRANCH}" ]] && echo "branch_exists=false" >> $GITHUB_ENV || echo "branch_exists=true" >> $GITHUB_ENV 34 | [[ -z "${REMOTE_BRANCH}" ]] && echo "Remote branch bump_$LATEST_TAG does not exist" || echo "Remote branch bump_$LATEST_TAG already exists" 35 | shell: bash 36 | 37 | - name: Get current version 38 | run: | 39 | CURRENT_VERSION=$(cat padd.sh | grep '^padd_version=' | cut -d'"' -f 2) 40 | echo "Current PADD version is $CURRENT_VERSION" 41 | echo "current_version=$CURRENT_VERSION" >> $GITHUB_ENV 42 | shell: bash 43 | 44 | - name: Create PR if versions don't match and branch does not exist 45 | if: (env.current_version != env.latest_tag) && (env.branch_exists == 'false') 46 | run: | 47 | LATEST_TAG=${{ env.latest_tag }} 48 | git config --global user.email "pralor-bot@users.noreply.github.com" 49 | git config --global user.name "pralor-bot" 50 | git checkout -b "bump_$LATEST_TAG" development 51 | sed -i "s/^padd_version=.*/padd_version=\"$LATEST_TAG\"/" padd.sh 52 | git commit -a -m "Bump version to $LATEST_TAG" 53 | git push --set-upstream origin "bump_$LATEST_TAG" 54 | gh pr create -B development -H "bump_$LATEST_TAG" --title "Bump version to ${LATEST_TAG}" --body 'Created by Github action' --label 'Internal' 55 | shell: bash 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary files generated by padd.sh # 2 | ######################################## 3 | piHoleVersion 4 | 5 | # Compiled source # 6 | ################### 7 | *.com 8 | *.class 9 | *.dll 10 | *.exe 11 | *.o 12 | *.so 13 | 14 | # Packages # 15 | ############ 16 | # it's better to unpack these files and commit the raw source 17 | # git has its own built in compression methods 18 | *.7z 19 | *.dmg 20 | *.gz 21 | *.iso 22 | *.jar 23 | *.rar 24 | *.tar 25 | *.zip 26 | 27 | # Logs and databases # 28 | ###################### 29 | *.log 30 | *.sql 31 | *.sqlite 32 | 33 | # OS generated files # 34 | ###################### 35 | .DS_Store 36 | .DS_Store? 37 | ._* 38 | .Spotlight-V100 39 | .Trashes 40 | ehthumbs.db 41 | Thumbs.db 42 | 43 | # Web / Atom Files # 44 | #################### 45 | *.ini 46 | .remote-sync.json 47 | sftp-config.json 48 | .vscode 49 | old 50 | *.sublime-* 51 | 52 | # Testing Files # 53 | ################# 54 | new\.sh 55 | net-test.sh 56 | padd.py 57 | -------------------------------------------------------------------------------- /.yamllint.conf: -------------------------------------------------------------------------------- 1 | rules: 2 | line-length: disable 3 | document-start: disable 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PADD 2 | 3 | PADD (formerly Chronometer2) is a more expansive version of the original chronometer.sh that is included with [Pi-Hole](https://pi-hole.net). PADD provides in-depth information about your Pi-hole. 4 | 5 | ![PADD Screenshot](https://pi-hole.github.io/graphics/Screenshots/padd.png) 6 | 7 | ***Note:** PADD has been adopted by the Pi-hole team, thanks to JPMCK for creating this helpful tool! 8 | 9 | ## Setup PADD 10 | 11 | - Get a copy of PADD by running: 12 | 13 | ```bash 14 | cd ~ 15 | wget -O padd.sh https://install.padd.sh 16 | ``` 17 | 18 | or 19 | 20 | ```bash 21 | cd ~ 22 | curl -sSL https://install.padd.sh -o padd.sh 23 | ``` 24 | 25 | - Make PADD executable by running 26 | 27 | ```bash 28 | sudo chmod +x padd.sh 29 | ``` 30 | 31 | ## Using PADD 32 | 33 | ### PADD on Pi-hole machine 34 | 35 | - Just run 36 | 37 | ```bash 38 | ./padd.sh 39 | ``` 40 | 41 | ### PADD from other machine 42 | 43 | With PADD v4.0.0 and Pi-hole v6 it is also possible to run PADD from a machine that is not running Pi-hole 44 | 45 | ```bash 46 | ./padd.sh --server 47 | ``` 48 | 49 | ### Authentication 50 | 51 | Pi-hole v6 uses a completely new API with a new authentication mechanism 52 | 53 | If you run PADD on the same machine as Pi-hole, it's possible to bypass authentication when your local user is member of the `pihole` group (specifically, if you can access `/etc/pihole/cli_pw). 54 | For details see [https://github.com/pi-hole/FTL/pull/1999](https://github.com/pi-hole/FTL/pull/1999) 55 | 56 | If this is not the case, PADD will ask you for your password and (if configured) your two factor authentication token. You can also pass those as arguments 57 | 58 | - password only 59 | 60 | ```bash 61 | ./padd.sh --secret 62 | ``` 63 | 64 | - with 2FA enabled 65 | 66 | ```bash 67 | ./padd.sh --secret --2fa <2fa> 68 | ``` 69 | 70 | ### PADD with Pi-hole in a Docker Container 71 | 72 | - If you're running Pi-hole in the official Docker Container, `padd.sh` is pre-installed and named `padd`. It can be used with the following command: 73 | 74 | ```bash 75 | docker exec -it padd [padd_options] 76 | ``` 77 | 78 | ### PADD on PiTFT screen 79 | 80 | _Instructions for how to setup PiTFT screen can be found [here](https://learn.adafruit.com/adafruit-pitft-3-dot-5-touch-screen-for-raspberry-pi/easy-install-2)_ 81 | 82 | - Set PADD to auto run on the PiTFT screen by adding the following to the end of `~/.bashrc`: 83 | 84 | ```bash 85 | # Run PADD 86 | # If we’re on the PiTFT screen (ssh is xterm) 87 | if [ "$TERM" == "linux" ] ; then 88 | while : 89 | do 90 | ./padd.sh 91 | sleep 1 92 | done 93 | fi 94 | ``` 95 | 96 | One line version 97 | 98 | ```bash 99 | cd ~ ; echo "if [ \"\$TERM\" == \"linux\" ] ; then\n while :\n do\n ./padd.sh\n sleep 1\n done\nfi" | tee ~/.bashrc -a 100 | ``` 101 | 102 | - Reboot your Pi-Hole by running `sudo reboot`. PADD should now run on PiTFT Screen when your Pi-Hole has completed booting. 103 | 104 | #### (Optional) Put the PiTFT Display to Sleep at Night 105 | 106 | _If you don't want your PiTFT on all night when you are asleep, you can put it to sleep! (Note: **these instructions only apply to a PiTFT**.)_ 107 | 108 | - To do so, edit cron as root (`sudo crontab -e`) and add the following: 109 | 110 | ```bash 111 | # PiTFT+ SLEEPY TIME 112 | # Turn off the PiTFT+ at midnight 113 | 00 00 * * * sh -c 'echo "0" > /sys/class/backlight/soc\:backlight/brightness' 114 | # Turn on the PiTFT+ at 8:00 am 115 | 00 08 * * * sh -c 'echo "1" > /sys/class/backlight/soc\:backlight/brightness' 116 | ``` 117 | 118 | ## Updating PADD 119 | 120 | - Simply run 121 | 122 | ```bash 123 | ./padd.sh -u 124 | ``` 125 | 126 | - or run the same commands you used to install 127 | 128 | ```bash 129 | cd ~ 130 | wget -O padd.sh https://install.padd.sh 131 | ``` 132 | 133 | or 134 | 135 | ```bash 136 | cd ~ 137 | curl -sSL https://install.padd.sh -o padd.sh 138 | ``` 139 | 140 | ## Sizes 141 | 142 | PADD will display on screens that anywhere from 20x10 characters to over 80x26 characters. 143 | 144 | As your screen gets smaller, you’ll be presented with less information… however, you’ll always get the most important details: 145 | 146 | - The status of your Pi-hole (is it online, in need of an update?), 147 | - How many ads have been blocked, 148 | - Your hostname and IP, and 149 | - Your CPU’s current load. 150 | 151 | It will also run in the following modes (shown further below): 152 | 153 | - Pico: 20x10 characters 154 | - Nano: 24x12 characters 155 | - Micro: 30x16 characters 156 | - Mini: 40x18 characters 157 | - Tiny: 53x20 characters 158 | - Slim: 60x21 characters 159 | - Regular: 60x22 characters (takes up the entire screen on a 3.5" Adafruit PiTFT using the Terminal font at 8x14.) 160 | - Mega: 80x26 characters 161 | 162 | ### Sizing Your PADD 163 | 164 | How PADD will display on your screen depends on the size of the screen in _characters_, not _pixels_! PADD doesn’t care if it is running on a 5k Retina display on your $5,000 iMac Pro or on a $5 display you bought on eBay. 165 | 166 | If you want to change how PADD displays on a small display attached to your Raspberry Pi, use 167 | 168 | ```bash 169 | sudo dpkg-reconfigure console-setup 170 | ``` 171 | 172 | to configure your font settings to an ideal size for you. 173 | 174 | If you want to change how PADD displays through a terminal emulator (PuTTY, Terminal.app, iTerm2, etc.), resize your window or play with font sizes in your app of choice. 175 | 176 | ### The Sizes 177 | 178 | ![PADD Sizes GIF](https://github.com/pi-hole/graphics/blob/master/PADD/PADDsizes.gif) 179 | -------------------------------------------------------------------------------- /padd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC1091 3 | 4 | # Ignore warning about `local` being undefinded in POSIX 5 | # shellcheck disable=SC3043 6 | # https://github.com/koalaman/shellcheck/wiki/SC3043#exceptions 7 | 8 | # PADD 9 | # A more advanced version of the chronometer provided with Pihole 10 | 11 | # SETS LOCALE 12 | export LC_ALL=C 13 | export LC_NUMERIC=C 14 | 15 | ############################################ VARIABLES ############################################# 16 | 17 | # VERSION 18 | padd_version="v4.0.0" 19 | 20 | # LastChecks 21 | LastCheckPADDInformation=$(date +%s) 22 | LastCheckFullInformation=$(date +%s) 23 | LastCheckNetworkInformation=$(date +%s) 24 | 25 | # padd_data holds the data returned by FTL's /padd endpoint globally 26 | padd_data="" 27 | 28 | # COLORS 29 | CSI="$(printf '\033')[" # Control Sequence Introducer 30 | red_text="${CSI}91m" # Red 31 | green_text="${CSI}92m" # Green 32 | yellow_text="${CSI}93m" # Yellow 33 | blue_text="${CSI}94m" # Blue 34 | magenta_text="${CSI}95m" # Magenta 35 | cyan_text="${CSI}96m" # Cyan 36 | reset_text="${CSI}0m" # Reset to default 37 | clear_line="${CSI}0K" # Clear the current line to the right to wipe any artifacts remaining from last print 38 | 39 | # STYLES 40 | bold_text="${CSI}1m" 41 | blinking_text="${CSI}5m" 42 | dim_text="${CSI}2m" 43 | 44 | # CHECK BOXES 45 | check_box_good="[${green_text}✓${reset_text}]" # Good 46 | check_box_bad="[${red_text}✗${reset_text}]" # Bad 47 | check_box_disabled="[${blue_text}-${reset_text}]" # Disabled, but not an error 48 | check_box_question="[${yellow_text}?${reset_text}]" # Question / ? 49 | check_box_info="[${yellow_text}i${reset_text}]" # Info / i 50 | 51 | # PICO STATUSES 52 | pico_status_ok="${check_box_good} Sys. OK" 53 | pico_status_update="${check_box_info} Update" 54 | pico_status_hot="${check_box_bad} Sys. Hot!" 55 | pico_status_off="${check_box_info} No blck" 56 | pico_status_ftl_down="${check_box_bad} No CXN" 57 | pico_status_dns_down="${check_box_bad} DNS Down" 58 | 59 | # MINI STATUS 60 | mini_status_ok="${check_box_good} System OK" 61 | mini_status_update="${check_box_info} Update avail." 62 | mini_status_hot="${check_box_bad} System is hot!" 63 | mini_status_off="${check_box_info} No blocking!" 64 | mini_status_ftl_down="${check_box_bad} No connection!" 65 | mini_status_dns_down="${check_box_bad} DNS off!" 66 | 67 | # REGULAR STATUS 68 | full_status_ok="${check_box_good} System is healthy" 69 | full_status_update="${check_box_info} Updates are available" 70 | full_status_hot="${check_box_bad} System is hot!" 71 | full_status_off="${check_box_info} Blocking is disabled" 72 | full_status_ftl_down="${check_box_bad} No connection!" 73 | full_status_dns_down="${check_box_bad} DNS is off!" 74 | 75 | # MEGA STATUS 76 | mega_status_ok="${check_box_good} Your system is healthy" 77 | mega_status_update="${check_box_info} Updates are available" 78 | mega_status_hot="${check_box_bad} Your system is hot!" 79 | mega_status_off="${check_box_info} Blocking is disabled!" 80 | mega_status_ftl_down="${check_box_bad} No connection to FTL!" 81 | mega_status_dns_down="${check_box_bad} Pi-hole's DNS server is off!" 82 | 83 | # TINY STATUS 84 | tiny_status_ok="${check_box_good} System is healthy" 85 | tiny_status_update="${check_box_info} Updates are available" 86 | tiny_status_hot="${check_box_bad} System is hot!" 87 | tiny_status_off="${check_box_info} Blocking is disabled" 88 | tiny_status_ftl_down="${check_box_bad} No connection to FTL!" 89 | tiny_status_dns_down="${check_box_bad} DNS is off!" 90 | 91 | # Text only "logos" 92 | padd_text="${green_text}${bold_text}PADD${reset_text}" 93 | 94 | # PADD logos - regular and retro 95 | padd_logo_1="${bold_text}${green_text} __ __ __ ${reset_text}" 96 | padd_logo_2="${bold_text}${green_text}|__) /\\ | \\| \\ ${reset_text}" 97 | padd_logo_3="${bold_text}${green_text}| /--\\|__/|__/ ${reset_text}" 98 | padd_logo_retro_1="${bold_text} ${yellow_text}_${green_text}_ ${blue_text}_${magenta_text}_ ${yellow_text}_${green_text}_ ${reset_text}" 99 | 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}" 100 | 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}" 101 | 102 | ############################################# FTL ################################################## 103 | 104 | TestAPIAvailability() { 105 | 106 | local chaos_api_list authResponse cmdResult digReturnCode authStatus authData 107 | 108 | # Query the API URLs from FTL using CHAOS TXT 109 | # The result is a space-separated enumeration of full URLs 110 | # e.g., "http://localhost:80/api" or "https://domain.com:443/api" 111 | if [ -z "${SERVER}" ] || [ "${SERVER}" = "localhost" ] || [ "${SERVER}" = "127.0.0.1" ]; then 112 | # --server was not set or set to local, assuming we're running locally 113 | cmdResult="$(dig +short chaos txt local.api.ftl @localhost 2>&1; echo $?)" 114 | else 115 | # --server was set, try to get response from there 116 | cmdResult="$(dig +short chaos txt domain.api.ftl @"${SERVER}" 2>&1; echo $?)" 117 | fi 118 | 119 | # Gets the return code of the dig command (last line) 120 | # We can't use${cmdResult##*$'\n'*} here as $'..' is not POSIX 121 | digReturnCode="$(echo "${cmdResult}" | tail -n 1)" 122 | 123 | if [ ! "${digReturnCode}" = "0" ]; then 124 | # If the query was not successful 125 | moveXOffset; echo "API not available. Please check server address and connectivity" 126 | exit 1 127 | else 128 | # Dig returned 0 (success), so get the actual response (first line) 129 | chaos_api_list="$(echo "${cmdResult}" | head -n 1)" 130 | fi 131 | 132 | # Iterate over space-separated list of URLs 133 | while [ -n "${chaos_api_list}" ]; do 134 | # Get the first URL 135 | API_URL="${chaos_api_list%% *}" 136 | # Strip leading and trailing quotes 137 | API_URL="${API_URL%\"}" 138 | API_URL="${API_URL#\"}" 139 | 140 | # Test if the API is available at this URL 141 | authResponse=$(curl --connect-timeout 2 -skS -w "%{http_code}" "${API_URL}auth") 142 | 143 | # authStatus are the last 3 characters 144 | # not using ${authResponse#"${authResponse%???}"}" here because it's extremely slow on big responses 145 | authStatus=$(printf "%s" "${authResponse}" | tail -c 3) 146 | # data is everything from response without the last 3 characters 147 | authData=$(printf %s "${authResponse%???}") 148 | 149 | # Test if http status code was 200 (OK) or 401 (authentication required) 150 | if [ ! "${authStatus}" = 200 ] && [ ! "${authStatus}" = 401 ]; then 151 | # API is not available at this port/protocol combination 152 | API_PORT="" 153 | else 154 | # API is available at this URL combination 155 | 156 | if [ "${authStatus}" = 200 ]; then 157 | # API is available without authentication 158 | needAuth=false 159 | fi 160 | 161 | # Check if 2FA is required 162 | needTOTP=$(echo "${authData}"| jq --raw-output .session.totp 2>/dev/null) 163 | 164 | break 165 | fi 166 | 167 | # Remove the first URL from the list 168 | local last_api_list 169 | last_api_list="${chaos_api_list}" 170 | chaos_api_list="${chaos_api_list#* }" 171 | 172 | # If the list did not change, we are at the last element 173 | if [ "${last_api_list}" = "${chaos_api_list}" ]; then 174 | # Remove the last element 175 | chaos_api_list="" 176 | fi 177 | done 178 | 179 | # if API_PORT is empty, no working API port was found 180 | if [ -n "${API_PORT}" ]; then 181 | moveXOffset; echo "API not available at: ${API_URL}" 182 | moveXOffset; echo "Exiting." 183 | exit 1 184 | fi 185 | } 186 | 187 | LoginAPI() { 188 | # Exit early if no authentication is required 189 | if [ "${needAuth}" = false ]; then 190 | moveXOffset; echo "No authentication required." 191 | return 192 | fi 193 | 194 | # Try to read the CLI password (if enabled and readable by the current user) 195 | if [ -r /etc/pihole/cli_pw ]; then 196 | password=$(cat /etc/pihole/cli_pw) 197 | # If we can read the CLI password, we can skip 2FA even when it's required otherwise 198 | needTOTP=false 199 | fi 200 | 201 | if [ -z "${password}" ]; then 202 | # no password was supplied as argument or read from CLI file 203 | moveXOffset; echo "No password supplied. Please enter your password:" 204 | # secretly read the password 205 | moveXOffset; secretRead; printf '\n' 206 | fi 207 | 208 | if [ "${needTOTP}" = true ] && [ -z "${totp}" ]; then 209 | # 2FA required, but no TOTP was supplied as argument 210 | moveXOffset; echo "Please enter the correct second factor." 211 | moveXOffset; echo "(Can be any number if you used the app password)" 212 | moveXOffset; read -r totp 213 | fi 214 | 215 | # Try to authenticate using the supplied password (CLI file, argument or user input) and TOTP 216 | Authenticate 217 | 218 | # Try to login again until the session is valid 219 | while [ ! "${validSession}" = true ] ; do 220 | moveXOffset; echo "Authentication failed." 221 | 222 | # Print the error message if there is one 223 | if [ ! "${sessionError}" = "null" ]; then 224 | moveXOffset; echo "Error: ${sessionError}" 225 | fi 226 | # Print the session message if there is one 227 | if [ ! "${sessionMessage}" = "null" ]; then 228 | moveXOffset; echo "Error: ${sessionMessage}" 229 | fi 230 | 231 | moveXOffset; echo "Please enter the correct password:" 232 | 233 | # secretly read the password 234 | moveXOffset; secretRead; printf '\n' 235 | 236 | if [ "${needTOTP}" = true ]; then 237 | moveXOffset; echo "Please enter the correct second factor:" 238 | moveXOffset; echo "(Can be any number if you used the app password)" 239 | moveXOffset; read -r totp 240 | fi 241 | 242 | # Try to authenticate again 243 | Authenticate 244 | done 245 | 246 | # Loop exited, authentication was successful 247 | moveXOffset; echo "Authentication successful." 248 | 249 | } 250 | 251 | DeleteSession() { 252 | # if a valid Session exists (no password required or successful authenthication) and 253 | # SID is not null (successful authenthication only), delete the session 254 | if [ "${validSession}" = true ] && [ ! "${SID}" = null ]; then 255 | # Try to delete the session. Omit the output, but get the http status code 256 | deleteResponse=$(curl --connect-timeout 2 -skS -o /dev/null -w "%{http_code}" -X DELETE "${API_URL}auth" -H "Accept: application/json" -H "sid: ${SID}") 257 | 258 | printf "\n\n" 259 | case "${deleteResponse}" in 260 | "204") moveXOffset; printf "%b" "Session successfully deleted.\n";; 261 | "401") moveXOffset; printf "%b" "Logout attempt without a valid session. Unauthorized!\n";; 262 | esac; 263 | else 264 | # no session to delete, just print a newline for nicer output 265 | echo 266 | fi 267 | 268 | } 269 | 270 | Authenticate() { 271 | sessionResponse="$(curl --connect-timeout 2 -skS -X POST "${API_URL}auth" --user-agent "PADD ${padd_version}" --data "{\"password\":\"${password}\", \"totp\":${totp:-null}}" )" 272 | 273 | if [ -z "${sessionResponse}" ]; then 274 | moveXOffset; echo "No response from FTL server. Please check connectivity and use the options to set the API URL" 275 | moveXOffset; echo "Usage: $0 [--server ]" 276 | exit 1 277 | fi 278 | # obtain validity, session ID and sessionMessage from session response 279 | validSession=$(echo "${sessionResponse}"| jq .session.valid 2>/dev/null) 280 | SID=$(echo "${sessionResponse}"| jq --raw-output .session.sid 2>/dev/null) 281 | sessionMessage=$(echo "${sessionResponse}"| jq --raw-output .session.message 2>/dev/null) 282 | 283 | # obtain the error message from the session response 284 | sessionError=$(echo "${sessionResponse}"| jq --raw-output .error.message 2>/dev/null) 285 | } 286 | 287 | GetFTLData() { 288 | local response 289 | local data 290 | local status 291 | 292 | # get the data from querying the API as well as the http status code 293 | response=$(curl --connect-timeout 2 -sk -w "%{http_code}" -X GET "${API_URL}$1$2" -H "Accept: application/json" -H "sid: ${SID}" ) 294 | 295 | # status are the last 3 characters 296 | # not using ${response#"${response%???}"}" here because it's extremely slow on big responses 297 | status=$(printf "%s" "${response}" | tail -c 3) 298 | # data is everything from response without the last 3 characters 299 | data=$(printf %s "${response%???}") 300 | 301 | if [ "${status}" = 200 ]; then 302 | echo "${data}" 303 | elif [ "${status}" = 000 ]; then 304 | # connection lost 305 | echo "000" 306 | elif [ "${status}" = 401 ]; then 307 | # unauthorized 308 | echo "401" 309 | fi 310 | } 311 | 312 | 313 | ############################################# GETTERS ############################################## 314 | 315 | GetPADDData() { 316 | local response 317 | response=$(GetFTLData "padd" "$1") 318 | 319 | if [ "${response}" = 000 ]; then 320 | # connection lost 321 | padd_data="000" 322 | elif [ "${response}" = 401 ]; then 323 | # unauthorized 324 | padd_data="401" 325 | else 326 | # Iterate over all the leaf paths in the JSON object and creates key-value 327 | # pairs in the format "key=value". Nested objects are flattened using the dot 328 | # notation, e.g., { "a": { "b": 1 } } becomes "a.b=1". 329 | # We cannot use leaf_paths here as it was deprecated in jq 1.6 and removed in 330 | # current master 331 | # Using "paths(scalars | true)" will return null and false values. 332 | # We also check if the value is exactly `null` and, in this case, return the 333 | # string "null", as jq would return an empty string for nulls. 334 | 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) 335 | fi 336 | } 337 | 338 | GetPADDValue() { 339 | echo "$padd_data" | sed -n "s/^$1=//p" 2>/dev/null 340 | } 341 | 342 | GetSummaryInformation() { 343 | if [ "${connection_down_flag}" = true ]; then 344 | clients="N/A" 345 | blocking_enabled="N/A" 346 | domains_being_blocked="N/A" 347 | dns_queries_today="N/A" 348 | ads_blocked_today="N/A" 349 | ads_percentage_today="N/A" 350 | cache_size="N/A" 351 | cache_evictions="N/A" 352 | cache_inserts="N/A" 353 | latest_blocked_raw="N/A" 354 | top_blocked_raw="N/A" 355 | top_domain_raw="N/A" 356 | top_client_raw="N/A" 357 | return 358 | fi 359 | 360 | 361 | clients=$(GetPADDValue active_clients) 362 | 363 | blocking_enabled=$(GetPADDValue blocking) 364 | 365 | domains_being_blocked_raw=$(GetPADDValue gravity_size) 366 | domains_being_blocked=$(printf "%.f" "${domains_being_blocked_raw}") 367 | 368 | dns_queries_today_raw=$(GetPADDValue queries.total) 369 | dns_queries_today=$(printf "%.f" "${dns_queries_today_raw}") 370 | 371 | ads_blocked_today_raw=$(GetPADDValue queries.blocked) 372 | ads_blocked_today=$(printf "%.f" "${ads_blocked_today_raw}") 373 | 374 | ads_percentage_today_raw=$(GetPADDValue queries.percent_blocked) 375 | ads_percentage_today=$(printf "%.1f" "${ads_percentage_today_raw}") 376 | 377 | cache_size=$(GetPADDValue cache.size) 378 | cache_evictions=$(GetPADDValue cache.evicted) 379 | cache_inserts=$(echo "${padd_data}"| GetPADDValue cache.inserted) 380 | 381 | latest_blocked_raw=$(GetPADDValue recent_blocked) 382 | 383 | top_blocked_raw=$(GetPADDValue top_blocked) 384 | 385 | top_domain_raw=$(GetPADDValue top_domain) 386 | 387 | top_client_raw=$(GetPADDValue top_client) 388 | } 389 | 390 | GetSystemInformation() { 391 | 392 | if [ "${connection_down_flag}" = true ]; then 393 | system_uptime_raw=0 394 | temperature="N/A" 395 | temp_heatmap=${reset_text} 396 | 397 | cpu_load_1="N/A" 398 | cpu_load_5="N/A" 399 | cpu_load_15="N/A" 400 | cpu_load_1_heatmap=${reset_text} 401 | cpu_load_5_heatmap=${reset_text} 402 | cpu_load_15_heatmap=${reset_text} 403 | cpu_percent=0 404 | 405 | memory_percent=0 406 | memory_heatmap=${reset_text} 407 | 408 | sys_model="N/A" 409 | return 410 | fi 411 | 412 | # System uptime 413 | system_uptime_raw=$(GetPADDValue system.uptime) 414 | 415 | # CPU temperature and unit 416 | cpu_temp_raw=$(GetPADDValue sensors.cpu_temp) 417 | cpu_temp=$(printf "%.1f" "${cpu_temp_raw}") 418 | temp_unit=$(echo "${padd_data}" | GetPADDValue sensors.unit) 419 | 420 | # Temp + Unit 421 | if [ "${temp_unit}" = "C" ]; then 422 | temperature="${cpu_temp}°${temp_unit}" 423 | # no conversion needed 424 | cpu_temp_celsius="$(echo "${cpu_temp}" | awk -F '.' '{print $1}')" 425 | elif [ "${temp_unit}" = "F" ]; then 426 | temperature="${cpu_temp}°${temp_unit}" 427 | # convert to Celsius for limit checking 428 | cpu_temp_celsius="$(echo "${cpu_temp}" | awk '{print ($1-32) * 5 / 9}' | awk -F '.' '{print $1}')" 429 | elif [ "${temp_unit}" = "K" ]; then 430 | # no ° for Kelvin 431 | temperature="${cpu_temp}${temp_unit}" 432 | # convert to Celsius for limit checking 433 | cpu_temp_celsius="$(echo "${cpu_temp}" | awk '{print $1 - 273.15}' | awk -F '.' '{print $1}')" 434 | else # unknown unit 435 | temperature="${cpu_temp}°?" 436 | # no conversion needed 437 | cpu_temp_celsius=0 438 | fi 439 | 440 | # CPU temperature heatmap 441 | hot_flag=false 442 | # If we're getting close to 85°C... (https://www.raspberrypi.org/blog/introducing-turbo-mode-up-to-50-more-performance-for-free/) 443 | if [ "${cpu_temp_celsius}" -gt 80 ]; then 444 | temp_heatmap=${blinking_text}${red_text} 445 | # set flag to change the status message in SetStatusMessage() 446 | hot_flag=true 447 | elif [ "${cpu_temp_celsius}" -gt 70 ]; then 448 | temp_heatmap=${magenta_text} 449 | elif [ "${cpu_temp_celsius}" -gt 60 ]; then 450 | temp_heatmap=${blue_text} 451 | else 452 | temp_heatmap=${cyan_text} 453 | fi 454 | 455 | # CPU, load, heatmap 456 | core_count=$(GetPADDValue system.cpu.nprocs) 457 | cpu_load_1=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[0])") 458 | cpu_load_5=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[1])") 459 | cpu_load_15=$(printf %.2f "$(GetPADDValue system.cpu.load.raw.[2])") 460 | cpu_load_1_heatmap=$(HeatmapGenerator "${cpu_load_1}" "${core_count}") 461 | cpu_load_5_heatmap=$(HeatmapGenerator "${cpu_load_5}" "${core_count}") 462 | cpu_load_15_heatmap=$(HeatmapGenerator "${cpu_load_15}" "${core_count}") 463 | cpu_percent=$(printf %.1f "$(GetPADDValue system.cpu.load.percent.0)") 464 | 465 | # Memory use, heatmap and bar 466 | memory_percent_raw="$(GetPADDValue system.memory.ram.%used)" 467 | memory_percent=$(printf %.1f "${memory_percent_raw}") 468 | memory_heatmap="$(HeatmapGenerator "${memory_percent}")" 469 | 470 | # Get device model 471 | sys_model="$(GetPADDValue host_model)" 472 | 473 | # DOCKER_VERSION is set during GetVersionInformation, so this needs to run first during startup 474 | if [ ! "${DOCKER_VERSION}" = "null" ]; then 475 | # Docker image 476 | sys_model="Container" 477 | fi 478 | 479 | # Cleaning device model from useless OEM information 480 | sys_model=$(filterModel "${sys_model}") 481 | 482 | # FTL returns null if device information is not available 483 | if [ -z "$sys_model" ] || [ "$sys_model" = "null" ]; then 484 | sys_model="N/A" 485 | fi 486 | } 487 | 488 | GetNetworkInformation() { 489 | if [ "${connection_down_flag}" = true ]; then 490 | iface_name="N/A" 491 | pi_ip4_addr="N/A" 492 | pi_ip6_addr="N/A" 493 | ipv6_status="N/A" 494 | ipv6_heatmap=${reset_text} 495 | ipv6_check_box=${check_box_question} 496 | 497 | dhcp_status="N/A" 498 | dhcp_heatmap=${reset_text} 499 | dhcp_range="N/A" 500 | dhcp_range_heatmap=${reset_text} 501 | dhcp_ipv6_status="N/A" 502 | dhcp_ipv6_heatmap=${reset_text} 503 | 504 | pi_hostname="N/A" 505 | full_hostname="N/A" 506 | 507 | dns_count="N/A" 508 | dns_information="N/A" 509 | 510 | dnssec_status="N/A" 511 | dnssec_heatmap=${reset_text} 512 | 513 | conditional_forwarding_status="N/A" 514 | conditional_forwarding_heatmap=${reset_text} 515 | 516 | tx_bytes="N/A" 517 | rx_bytes="N/A" 518 | return 519 | fi 520 | 521 | gateway_v4_iface=$(GetPADDValue iface.v4.name) 522 | gateway_v6_iface=$(GetPADDValue iface.v4.name) 523 | 524 | # Get IPv4 address of the default interface 525 | pi_ip4_addrs="$(GetPADDValue iface.v4.num_addrs)" 526 | pi_ip4_addr="$(GetPADDValue iface.v4.addr)" 527 | if [ "${pi_ip4_addrs}" -eq 0 ]; then 528 | # No IPv4 address available 529 | pi_ip4_addr="N/A" 530 | elif [ "${pi_ip4_addrs}" -eq 1 ]; then 531 | # One IPv4 address available 532 | : # Do nothing as the address is already set 533 | else 534 | # More than one IPv4 address available 535 | pi_ip4_addr="${pi_ip4_addr}+" 536 | fi 537 | 538 | # Get IPv6 address of the default interface 539 | pi_ip6_addrs="$(GetPADDValue iface.v6.num_addrs)" 540 | pi_ip6_addr="$(GetPADDValue iface.v6.addr)" 541 | if [ "${pi_ip6_addrs}" -eq 0 ]; then 542 | # No IPv6 address available 543 | pi_ip6_addr="N/A" 544 | ipv6_check_box=${check_box_disabled} 545 | ipv6_status="Disabled" 546 | ipv6_heatmap=${blue_text} 547 | elif [ "${pi_ip6_addrs}" -eq 1 ]; then 548 | # One IPv6 address available 549 | ipv6_check_box=${check_box_good} 550 | ipv6_status="Enabled" 551 | ipv6_heatmap=${green_text} 552 | else 553 | # More than one IPv6 address available 554 | pi_ip6_addr="${pi_ip6_addr}+" 555 | ipv6_check_box=${check_box_good} 556 | ipv6_status="Enabled" 557 | ipv6_heatmap=${green_text} 558 | fi 559 | 560 | # Is Pi-Hole acting as the DHCP server? 561 | DHCP_ACTIVE="$(GetPADDValue config.dhcp_active )" 562 | 563 | if [ "${DHCP_ACTIVE}" = "true" ]; then 564 | DHCP_START="$(GetPADDValue config.dhcp_start)" 565 | DHCP_END="$(GetPADDValue config.dhcp_end)" 566 | 567 | dhcp_status="Enabled" 568 | dhcp_range="${DHCP_START} - ${DHCP_END}" 569 | dhcp_range_heatmap=${reset_text} 570 | dhcp_heatmap=${green_text} 571 | dhcp_check_box=${check_box_good} 572 | 573 | # Is DHCP handling IPv6? 574 | DHCP_IPv6="$(GetPADDValue config.dhcp_ipv6)" 575 | if [ "${DHCP_IPv6}" = "true" ]; then 576 | dhcp_ipv6_status="Enabled" 577 | dhcp_ipv6_heatmap=${green_text} 578 | else 579 | dhcp_ipv6_status="Disabled" 580 | dhcp_ipv6_heatmap=${blue_text} 581 | fi 582 | else 583 | dhcp_status="Disabled" 584 | dhcp_heatmap=${blue_text} 585 | dhcp_check_box=${check_box_disabled} 586 | dhcp_range="N/A" 587 | 588 | dhcp_ipv6_status="N/A" 589 | dhcp_range_heatmap=${yellow_text} 590 | dhcp_ipv6_heatmap=${yellow_text} 591 | fi 592 | 593 | # Get hostname 594 | pi_hostname="$(GetPADDValue node_name)" 595 | full_hostname=${pi_hostname} 596 | # when PI-hole is the DHCP server, append the domain to the hostname 597 | if [ "${DHCP_ACTIVE}" = "true" ]; then 598 | PIHOLE_DOMAIN="$(GetPADDValue config.dns_domain)" 599 | if [ -n "${PIHOLE_DOMAIN}" ]; then 600 | count=${pi_hostname}"."${PIHOLE_DOMAIN} 601 | count=${#count} 602 | if [ "${count}" -lt "18" ]; then 603 | full_hostname=${pi_hostname}"."${PIHOLE_DOMAIN} 604 | fi 605 | fi 606 | fi 607 | 608 | # Get the number of configured upstream DNS servers 609 | dns_count="$(GetPADDValue config.dns_num_upstreams)" 610 | # if there's only one DNS server 611 | if [ "${dns_count}" -eq 1 ]; then 612 | dns_information="1 server" 613 | else 614 | dns_information="${dns_count} servers" 615 | fi 616 | 617 | 618 | # DNSSEC 619 | DNSSEC="$(GetPADDValue config.dns_dnssec)" 620 | if [ "${DNSSEC}" = "true" ]; then 621 | dnssec_status="Enabled" 622 | dnssec_heatmap=${green_text} 623 | else 624 | dnssec_status="Disabled" 625 | dnssec_heatmap=${blue_text} 626 | fi 627 | 628 | # Conditional forwarding 629 | CONDITIONAL_FORWARDING="$(GetPADDValue config.dns_revServer_active)" 630 | if [ "${CONDITIONAL_FORWARDING}" = "true" ]; then 631 | conditional_forwarding_status="Enabled" 632 | conditional_forwarding_heatmap=${green_text} 633 | else 634 | conditional_forwarding_status="Disabled" 635 | conditional_forwarding_heatmap=${blue_text} 636 | fi 637 | 638 | # Default interface data (use IPv4 interface - we cannot show both and assume they are the same) 639 | iface_name="${gateway_v4_iface}" 640 | tx_bytes="$(GetPADDValue iface.v4.tx_bytes.value)" 641 | tx_bytes_unit="$(GetPADDValue iface.v4.tx_bytes.unit)" 642 | tx_bytes=$(printf "%.1f %b" "${tx_bytes}" "${tx_bytes_unit}") 643 | 644 | rx_bytes="$(GetPADDValue iface.v4.rx_bytes.value)" 645 | rx_bytes_unit="$(GetPADDValue iface.v4.rx_bytes.unit)" 646 | rx_bytes=$(printf "%.1f %b" "${rx_bytes}" "${rx_bytes_unit}") 647 | 648 | # If IPv4 and IPv6 interfaces are not the same, add a "*" to the interface 649 | # name to highlight that there are two different interfaces and the 650 | # displayed statistics are only for the IPv4 interface, while the IPv6 651 | # address correctly corresponds to the default IPv6 interface 652 | if [ ! "${gateway_v4_iface}" = "${gateway_v6_iface}" ]; then 653 | iface_name="${iface_name}*" 654 | fi 655 | } 656 | 657 | GetPiholeInformation() { 658 | if [ "${connection_down_flag}" = true ]; then 659 | ftl_status="No connection" 660 | ftl_heatmap=${red_text} 661 | ftl_check_box=${check_box_bad} 662 | ftl_cpu="N/A" 663 | ftl_mem_percentage="N/A" 664 | dns_status="DNS offline" 665 | dns_heatmap=${red_text} 666 | dns_check_box=${check_box_bad} 667 | ftlPID="N/A" 668 | dns_down_flag=true 669 | 670 | return 671 | fi 672 | 673 | ftl_status="Running" 674 | ftl_heatmap=${green_text} 675 | ftl_check_box=${check_box_good} 676 | # Get FTL CPU and memory usage 677 | ftl_cpu_raw="$(GetPADDValue "%cpu")" 678 | ftl_mem_percentage_raw="$(GetPADDValue "%mem")" 679 | ftl_cpu="$(printf "%.1f" "${ftl_cpu_raw}")%" 680 | ftl_mem_percentage="$(printf "%.1f" "${ftl_mem_percentage_raw}")%" 681 | # Get Pi-hole (blocking) status 682 | ftl_dns_port=$(GetPADDValue config.dns_port) 683 | # Get FTL's current PID 684 | ftlPID="$(GetPADDValue pid)" 685 | 686 | 687 | 688 | # ${ftl_dns_port} == 0 DNS server part of dnsmasq disabled 689 | dns_down_flag=false 690 | if [ "${ftl_dns_port}" = 0 ]; then 691 | dns_status="DNS offline" 692 | dns_heatmap=${red_text} 693 | dns_check_box=${check_box_bad} 694 | # set flag to change the status message in SetStatusMessage() 695 | dns_down_flag=true 696 | else 697 | dns_check_box=${check_box_good} 698 | dns_status="Active" 699 | dns_heatmap=${green_text} 700 | fi 701 | } 702 | 703 | GetVersionInformation() { 704 | if [ "${connection_down_flag}" = true ]; then 705 | DOCKER_VERSION=null 706 | CORE_VERSION="N/A" 707 | WEB_VERSION="N/A" 708 | FTL_VERSION="N/A" 709 | core_version_heatmap=${reset_text} 710 | web_version_heatmap=${reset_text} 711 | ftl_version_heatmap=${reset_text} 712 | return 713 | fi 714 | 715 | out_of_date_flag=false 716 | 717 | # Gather DOCKER version information... 718 | # returns "null" if not running Pi-hole in Docker container 719 | DOCKER_VERSION="$(GetPADDValue version.docker.local)" 720 | 721 | # If PADD is running inside docker, immediately return without checking for updated component versions 722 | if [ ! "${DOCKER_VERSION}" = "null" ] ; then 723 | GITHUB_DOCKER_VERSION="$(GetPADDValue version.docker.remote)" 724 | docker_version_converted="$(VersionConverter "${DOCKER_VERSION}")" 725 | docker_version_latest_converted="$(VersionConverter "${GITHUB_DOCKER_VERSION}")" 726 | 727 | # Note: the version comparison will fail for any Docker tag not following a 'YYYY.MM.VV' scheme 728 | # e.g. 'nightly', 'beta', 'v6-pre-alpha' and might set a false out_of_date_flag 729 | # As those versions are not meant to be used in production, we ignore this small bug 730 | if [ "${docker_version_converted}" -lt "${docker_version_latest_converted}" ]; then 731 | out_of_date_flag="true" 732 | docker_version_heatmap=${red_text} 733 | else 734 | docker_version_heatmap=${green_text} 735 | fi 736 | return 737 | fi 738 | 739 | # Gather core version information... 740 | CORE_BRANCH="$(GetPADDValue version.core.local.branch)" 741 | CORE_VERSION="$(GetPADDValue version.core.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" 742 | GITHUB_CORE_VERSION="$(GetPADDValue version.core.remmote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" 743 | CORE_HASH="$(GetPADDValue version.core.local.hash)" 744 | GITHUB_CORE_HASH="$(GetPADDValue version.core.remote.hash)" 745 | 746 | if [ "${CORE_BRANCH}" = "master" ]; then 747 | core_version_converted="$(VersionConverter "${CORE_VERSION}")" 748 | core_version_latest_converted=$(VersionConverter "${GITHUB_CORE_VERSION}") 749 | 750 | if [ "${core_version_converted}" -lt "${core_version_latest_converted}" ]; then 751 | out_of_date_flag="true" 752 | core_version_heatmap=${red_text} 753 | else 754 | core_version_heatmap=${green_text} 755 | fi 756 | else 757 | # Custom branch 758 | if [ -z "${CORE_BRANCH}" ]; then 759 | # Branch name is empty, something went wrong 760 | core_version_heatmap=${red_text} 761 | CORE_VERSION="?" 762 | else 763 | if [ "${CORE_HASH}" = "${GITHUB_CORE_HASH}" ]; then 764 | # up-to-date 765 | core_version_heatmap=${green_text} 766 | else 767 | # out-of-date 768 | out_of_date_flag="true" 769 | core_version_heatmap=${red_text} 770 | fi 771 | # shorten common branch names (fix/, tweak/, new/) 772 | # use the first 7 characters of the branch name as version 773 | CORE_VERSION="$(printf '%s' "$CORE_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" 774 | fi 775 | fi 776 | 777 | # Gather web version information... 778 | WEB_VERSION="$(GetPADDValue version.web.local.version)" 779 | 780 | if [ ! "$WEB_VERSION" = "null" ]; then 781 | WEB_BRANCH="$(GetPADDValue version.web.local.branch)" 782 | WEB_VERSION="$(GetPADDValue version.web.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" 783 | GITHUB_WEB_VERSION="$(GetPADDValue version.web.remmote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" 784 | WEB_HASH="$(GetPADDValue version.web.local.hash)" 785 | GITHUB_WEB_HASH="$(GetPADDValue version.web.remote.hash)" 786 | 787 | if [ "${WEB_BRANCH}" = "master" ]; then 788 | web_version_converted="$(VersionConverter "${WEB_VERSION}")" 789 | web_version_latest_converted=$(VersionConverter "${GITHUB_WEB_VERSION}") 790 | 791 | if [ "${web_version_converted}" -lt "${web_version_latest_converted}" ]; then 792 | out_of_date_flag="true" 793 | web_version_heatmap=${red_text} 794 | else 795 | web_version_heatmap=${green_text} 796 | fi 797 | 798 | else 799 | # Custom branch 800 | if [ -z "${WEB_BRANCH}" ]; then 801 | # Branch name is empty, something went wrong 802 | web_version_heatmap=${red_text} 803 | WEB_VERSION="?" 804 | else 805 | if [ "${WEB_HASH}" = "${GITHUB_WEB_HASH}" ]; then 806 | # up-to-date 807 | web_version_heatmap=${green_text} 808 | else 809 | # out-of-date 810 | out_of_date_flag="true" 811 | web_version_heatmap=${red_text} 812 | fi 813 | # shorten common branch names (fix/, tweak/, new/) 814 | # use the first 7 characters of the branch name as version 815 | WEB_VERSION="$(printf '%s' "$WEB_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" 816 | fi 817 | fi 818 | else 819 | # Web interface not installed 820 | WEB_VERSION="N/A" 821 | web_version_heatmap=${yellow_text} 822 | fi 823 | 824 | # Gather FTL version information... 825 | FTL_BRANCH="$(GetPADDValue version.ftl.local.branch)" 826 | FTL_VERSION="$(GetPADDValue version.ftl.local.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" 827 | GITHUB_FTL_VERSION="$(GetPADDValue version.ftl.remmote.version | tr -d '[:alpha:]' | awk -F '-' '{printf $1}')" 828 | FTL_HASH="$(GetPADDValue version.ftl.local.hash)" 829 | GITHUB_FTL_HASH="$(GetPADDValue version.ftl.remote.hash)" 830 | 831 | 832 | if [ "${FTL_BRANCH}" = "master" ]; then 833 | ftl_version_converted="$(VersionConverter "${FTL_VERSION}")" 834 | ftl_version_latest_converted=$(VersionConverter "${GITHUB_FTL_VERSION}") 835 | 836 | if [ "${ftl_version_converted}" -lt "${ftl_version_latest_converted}" ]; then 837 | out_of_date_flag="true" 838 | ftl_version_heatmap=${red_text} 839 | else 840 | ftl_version_heatmap=${green_text} 841 | fi 842 | else 843 | # Custom branch 844 | if [ -z "${FTL_BRANCH}" ]; then 845 | # Branch name is empty, something went wrong 846 | ftl_version_heatmap=${red_text} 847 | FTL_VERSION="?" 848 | else 849 | if [ "${FTL_HASH}" = "${GITHUB_FTL_HASH}" ]; then 850 | # up-to-date 851 | ftl_version_heatmap=${green_text} 852 | else 853 | # out-of-date 854 | out_of_date_flag="true" 855 | ftl_version_heatmap=${red_text} 856 | fi 857 | # shorten common branch names (fix/, tweak/, new/) 858 | # use the first 7 characters of the branch name as version 859 | FTL_VERSION="$(printf '%s' "$FTL_BRANCH" | sed 's/fix\//f\//;s/new\//n\//;s/tweak\//t\//' | cut -c 1-7)" 860 | fi 861 | fi 862 | 863 | } 864 | 865 | GetPADDInformation() { 866 | # If PADD is running inside docker, immediately return without checking for an update 867 | if [ ! "${DOCKER_VERSION}" = "null" ]; then 868 | return 869 | fi 870 | 871 | # PADD version information... 872 | 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}')" 873 | # is PADD up-to-date? 874 | padd_out_of_date_flag=false 875 | if [ -z "${padd_version_latest}" ]; then 876 | padd_version_heatmap=${yellow_text} 877 | else 878 | padd_version_latest_converted="$(VersionConverter "${padd_version_latest}")" 879 | padd_version_converted=$(VersionConverter "${padd_version}") 880 | 881 | if [ "${padd_version_converted}" -lt "${padd_version_latest_converted}" ]; then 882 | padd_out_of_date_flag="true" 883 | padd_version_heatmap=${red_text} 884 | else 885 | # local and remote PADD version match or local is newer 886 | padd_version_heatmap=${green_text} 887 | fi 888 | fi 889 | } 890 | 891 | GenerateSizeDependendOutput() { 892 | if [ "$1" = "pico" ] || [ "$1" = "nano" ]; then 893 | ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 9 "color") 894 | 895 | elif [ "$1" = "micro" ]; then 896 | ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 10 "color") 897 | 898 | elif [ "$1" = "mini" ]; then 899 | ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 20 "color") 900 | 901 | latest_blocked=$(truncateString "$latest_blocked_raw" 29) 902 | top_blocked=$(truncateString "$top_blocked_raw" 29) 903 | 904 | elif [ "$1" = "tiny" ]; then 905 | ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 30 "color") 906 | 907 | latest_blocked=$(truncateString "$latest_blocked_raw" 41) 908 | top_blocked=$(truncateString "$top_blocked_raw" 41) 909 | top_domain=$(truncateString "$top_domain_raw" 41) 910 | top_client=$(truncateString "$top_client_raw" 41) 911 | 912 | elif [ "$1" = "regular" ] || [ "$1" = "slim" ]; then 913 | ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 40 "color") 914 | 915 | latest_blocked=$(truncateString "$latest_blocked_raw" 48) 916 | top_blocked=$(truncateString "$top_blocked_raw" 48) 917 | top_domain=$(truncateString "$top_domain_raw" 48) 918 | top_client=$(truncateString "$top_client_raw" 48) 919 | 920 | 921 | elif [ "$1" = "mega" ]; then 922 | ads_blocked_bar=$(BarGenerator "$ads_percentage_today" 30 "color") 923 | 924 | latest_blocked=$(truncateString "$latest_blocked_raw" 68) 925 | top_blocked=$(truncateString "$top_blocked_raw" 68) 926 | top_domain=$(truncateString "$top_domain_raw" 68) 927 | top_client=$(truncateString "$top_client_raw" 68) 928 | 929 | fi 930 | 931 | # System uptime 932 | if [ "$1" = "pico" ] || [ "$1" = "nano" ] || [ "$1" = "micro" ]; then 933 | system_uptime="$(convertUptime "${system_uptime_raw}" | awk -F ',' '{print $1 "," $2}')" 934 | else 935 | system_uptime="$(convertUptime "${system_uptime_raw}")" 936 | fi 937 | 938 | # Bar generations 939 | if [ "$1" = "mini" ]; then 940 | cpu_bar=$(BarGenerator "${cpu_percent}" 20) 941 | memory_bar=$(BarGenerator "${memory_percent}" 20) 942 | elif [ "$1" = "tiny" ]; then 943 | cpu_bar=$(BarGenerator "${cpu_percent}" 7) 944 | memory_bar=$(BarGenerator "${memory_percent}" 7) 945 | else 946 | cpu_bar=$(BarGenerator "${cpu_percent}" 10) 947 | memory_bar=$(BarGenerator "${memory_percent}" 10) 948 | fi 949 | } 950 | 951 | SetStatusMessage() { 952 | # depending on which flags are set, the "message field" shows a different output 953 | # 7 messages are possible (from highest to lowest priority): 954 | 955 | # - System is hot 956 | # - FTLDNS service is not running 957 | # - Pi-hole's DNS server is off (FTL running, but not providing DNS) 958 | # - Unable to determine Pi-hole blocking status 959 | # - Pi-hole blocking disabled 960 | # - Updates are available 961 | # - Everything is fine 962 | 963 | 964 | if [ "${hot_flag}" = true ]; then 965 | # Check if CPU temperature is high 966 | pico_status="${pico_status_hot}" 967 | mini_status="${mini_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" 968 | tiny_status="${tiny_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" 969 | full_status="${full_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" 970 | mega_status="${mega_status_hot} ${blinking_text}${red_text}${temperature}${reset_text}" 971 | 972 | elif [ "${connection_down_flag}" = true ]; then 973 | # Check if FTL is down 974 | pico_status=${pico_status_ftl_down} 975 | mini_status=${mini_status_ftl_down} 976 | tiny_status=${tiny_status_ftl_down} 977 | full_status=${full_status_ftl_down} 978 | mega_status=${mega_status_ftl_down} 979 | 980 | elif [ "${dns_down_flag}" = true ]; then 981 | # Check if DNS is down 982 | pico_status=${pico_status_dns_down} 983 | mini_status=${mini_status_dns_down} 984 | tiny_status=${tiny_status_dns_down} 985 | full_status=${full_status_dns_down} 986 | mega_status=${mega_status_dns_down} 987 | 988 | elif [ "${blocking_enabled}" = "disabled" ]; then 989 | # Check if blocking status is disabled 990 | pico_status=${pico_status_off} 991 | mini_status=${mini_status_off} 992 | tiny_status=${tiny_status_off} 993 | full_status=${full_status_off} 994 | mega_status=${mega_status_off} 995 | 996 | elif [ "${out_of_date_flag}" = "true" ] || [ "${padd_out_of_date_flag}" = "true" ]; then 997 | # Check if one of the components of Pi-hole (or PADD itself) is out of date 998 | pico_status=${pico_status_update} 999 | mini_status=${mini_status_update} 1000 | tiny_status=${tiny_status_update} 1001 | full_status=${full_status_update} 1002 | mega_status=${mega_status_update} 1003 | 1004 | elif [ "${blocking_enabled}" = "enabled" ]; then 1005 | # if we reach this point and blocking is enabled, everything is fine 1006 | pico_status=${pico_status_ok} 1007 | mini_status=${mini_status_ok} 1008 | tiny_status=${tiny_status_ok} 1009 | full_status=${full_status_ok} 1010 | mega_status=${mega_status_ok} 1011 | fi 1012 | } 1013 | 1014 | ############################################# PRINTERS ############################################# 1015 | 1016 | PrintLogo() { 1017 | if [ ! "${DOCKER_VERSION}" = "null" ]; then 1018 | version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" 1019 | else 1020 | 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}" 1021 | fi 1022 | 1023 | # Screen size checks 1024 | if [ "$1" = "pico" ]; then 1025 | printf "%s${clear_line}\n" "p${padd_text} ${pico_status}" 1026 | elif [ "$1" = "nano" ]; then 1027 | printf "%s${clear_line}\n" "n${padd_text} ${mini_status}" 1028 | elif [ "$1" = "micro" ]; then 1029 | printf "%s${clear_line}\n${clear_line}\n" "µ${padd_text} ${mini_status}" 1030 | elif [ "$1" = "mini" ]; then 1031 | printf "%s${clear_line}\n${clear_line}\n" "${padd_text}${dim_text}mini${reset_text} ${mini_status}" 1032 | elif [ "$1" = "tiny" ]; then 1033 | printf "%s${clear_line}\n" "${padd_text}${dim_text}tiny${reset_text} ${version_info}${reset_text}" 1034 | printf "%s${clear_line}\n" " PADD ${padd_version_heatmap}${padd_version}${reset_text} ${tiny_status}${reset_text}" 1035 | elif [ "$1" = "slim" ]; then 1036 | printf "%s${clear_line}\n${clear_line}\n" "${padd_text}${dim_text}slim${reset_text} ${full_status}" 1037 | elif [ "$1" = "regular" ] || [ "$1" = "slim" ]; then 1038 | printf "%s${clear_line}\n" "${padd_logo_1}" 1039 | printf "%s${clear_line}\n" "${padd_logo_2}${version_info}${reset_text}" 1040 | printf "%s${clear_line}\n${clear_line}\n" "${padd_logo_3}PADD ${padd_version_heatmap}${padd_version}${reset_text} ${full_status}${reset_text}" 1041 | # normal or not defined 1042 | else 1043 | printf "%s${clear_line}\n" "${padd_logo_retro_1}" 1044 | printf "%s${clear_line}\n" "${padd_logo_retro_2} ${version_info}, PADD ${padd_version_heatmap}${padd_version}${reset_text}" 1045 | printf "%s${clear_line}\n${clear_line}\n" "${padd_logo_retro_3} ${dns_check_box} DNS ${ftl_check_box} FTL ${mega_status}${reset_text}" 1046 | fi 1047 | } 1048 | 1049 | PrintDashboard() { 1050 | if [ ! "${DOCKER_VERSION}" = "null" ]; then 1051 | version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" 1052 | else 1053 | 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}" 1054 | fi 1055 | # Move cursor to (0,0). 1056 | printf '\e[H' 1057 | 1058 | # adds the y-offset 1059 | moveYOffset 1060 | 1061 | if [ "$1" = "pico" ]; then 1062 | # pico is a screen at least 20x10 (columns x lines) 1063 | moveXOffset; printf "%s${clear_line}\n" "p${padd_text} ${pico_status}" 1064 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ============${reset_text}" 1065 | moveXOffset; printf "%s${clear_line}\n" " [${ads_blocked_bar}] ${ads_percentage_today}%" 1066 | moveXOffset; printf "%s${clear_line}\n" " ${ads_blocked_today} / ${dns_queries_today}" 1067 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ============${reset_text}" 1068 | moveXOffset; printf "%s${clear_line}\n" " Hst: ${pi_hostname}" 1069 | moveXOffset; printf "%s${clear_line}\n" " IP: ${pi_ip4_addr}" 1070 | moveXOffset; printf "%s${clear_line}\n" " IPv6 ${ipv6_check_box} DHCP ${dhcp_check_box}" 1071 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}CPU ================${reset_text}" 1072 | moveXOffset; printf "%s${clear_line}" " [${cpu_load_1_heatmap}${cpu_bar}${reset_text}] ${cpu_percent}%" 1073 | elif [ "$1" = "nano" ]; then 1074 | # nano is a screen at least 24x12 (columns x lines) 1075 | moveXOffset; printf "%s${clear_line}\n" "n${padd_text} ${mini_status}" 1076 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ================${reset_text}" 1077 | moveXOffset; printf "%s${clear_line}\n" " DNS: ${dns_check_box} FTL: ${ftl_check_box}" 1078 | moveXOffset; printf "%s${clear_line}\n" " Blk: [${ads_blocked_bar}] ${ads_percentage_today}%" 1079 | moveXOffset; printf "%s${clear_line}\n" " Blk: ${ads_blocked_today} / ${dns_queries_today}" 1080 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ================${reset_text}" 1081 | moveXOffset; printf "%s${clear_line}\n" " Host: ${pi_hostname}" 1082 | moveXOffset; printf "%s${clear_line}\n" " IP: ${pi_ip4_addr}" 1083 | moveXOffset; printf "%s${clear_line}\n" " IPv6: ${ipv6_check_box} DHCP: ${dhcp_check_box}" 1084 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =================${reset_text}" 1085 | moveXOffset; printf "%s${clear_line}\n" " Up: ${system_uptime}" 1086 | moveXOffset; printf "%s${clear_line}" " CPU: [${cpu_load_1_heatmap}${cpu_bar}${reset_text}] ${cpu_percent}%" 1087 | elif [ "$1" = "micro" ]; then 1088 | # micro is a screen at least 30x16 (columns x lines) 1089 | moveXOffset; printf "%s${clear_line}\n" "µ${padd_text} ${mini_status}" 1090 | moveXOffset; printf "%s${clear_line}\n" "" 1091 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ======================${reset_text}" 1092 | moveXOffset; printf "%s${clear_line}\n" " DNS: ${dns_check_box} FTL: ${ftl_check_box}" 1093 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ========================${reset_text}" 1094 | moveXOffset; printf "%s${clear_line}\n" " Blckng: ${domains_being_blocked} domains" 1095 | moveXOffset; printf "%s${clear_line}\n" " Piholed: [${ads_blocked_bar}] ${ads_percentage_today}%" 1096 | moveXOffset; printf "%s${clear_line}\n" " Piholed: ${ads_blocked_today} / ${dns_queries_today}" 1097 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ======================${reset_text}" 1098 | moveXOffset; printf "%s${clear_line}\n" " Host: ${full_hostname}" 1099 | moveXOffset; printf "%s${clear_line}\n" " IP: ${pi_ip4_addr}" 1100 | moveXOffset; printf "%s${clear_line}\n" " IPv6: ${ipv6_check_box} DHCP: ${dhcp_check_box}" 1101 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =======================${reset_text}" 1102 | moveXOffset; printf "%s${clear_line}\n" " Uptime: ${system_uptime}" 1103 | moveXOffset; printf "%s${clear_line}\n" " Load: [${cpu_load_1_heatmap}${cpu_bar}${reset_text}] ${cpu_percent}%" 1104 | moveXOffset; printf "%s${clear_line}" " Memory: [${memory_heatmap}${memory_bar}${reset_text}] ${memory_percent}%" 1105 | elif [ "$1" = "mini" ]; then 1106 | # mini is a screen at least 40x18 (columns x lines) 1107 | moveXOffset; printf "%s${clear_line}\n" "${padd_text}${dim_text}mini${reset_text} ${mini_status}" 1108 | moveXOffset; printf "%s${clear_line}\n" "" 1109 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ================================${reset_text}" 1110 | moveXOffset; printf " %-9s${dns_heatmap}%-10s${reset_text} %-5s${ftl_heatmap}%-10s${reset_text}${clear_line}\n" "DNS:" "${dns_status}" "FTL:" "${ftl_status}" 1111 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ==================================${reset_text}" 1112 | moveXOffset; printf " %-9s%-29s${clear_line}\n" "Blckng:" "${domains_being_blocked} domains" 1113 | moveXOffset; printf " %-9s[%-20s] %-5s${clear_line}\n" "Piholed:" "${ads_blocked_bar}" "${ads_percentage_today}%" 1114 | moveXOffset; printf " %-9s%-29s${clear_line}\n" "Piholed:" "${ads_blocked_today} out of ${dns_queries_today}" 1115 | moveXOffset; printf " %-9s%-29s${clear_line}\n" "Latest:" "${latest_blocked}" 1116 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ================================${reset_text}" 1117 | moveXOffset; printf " %-9s%-16s%-5s%-9s${clear_line}\n" "Host:" "${full_hostname}" "DNS:" "${dns_information}" 1118 | moveXOffset; printf " %-9s%s${clear_line}\n" "IP:" "${pi_ip4_addr} (${iface_name})" 1119 | moveXOffset; printf " %-9s${ipv6_heatmap}%-10s${reset_text} %-8s${dhcp_heatmap}%-10s${reset_text}${clear_line}\n" "IPv6:" "${ipv6_status}" "DHCP:" "${dhcp_status}" 1120 | 1121 | moveXOffset; printf " %-9s%-4s%-12s%-4s%-5s${clear_line}\n" "Traffic:" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}" 1122 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =================================${reset_text}" 1123 | moveXOffset; printf " %-9s%-29s${clear_line}\n" "Uptime:" "${system_uptime}" 1124 | moveXOffset; printf "%s${clear_line}\n" " Load: [${cpu_load_1_heatmap}${cpu_bar}${reset_text}] ${cpu_percent}%" 1125 | moveXOffset; printf "%s${clear_line}" " Memory: [${memory_heatmap}${memory_bar}${reset_text}] ${memory_percent}%" 1126 | elif [ "$1" = "tiny" ]; then 1127 | # tiny is a screen at least 53x20 (columns x lines) 1128 | moveXOffset; printf "%s${clear_line}\n" "${padd_text}${dim_text}tiny${reset_text} ${version_info}${reset_text}" 1129 | moveXOffset; printf "%s${clear_line}\n" " PADD ${padd_version_heatmap}${padd_version}${reset_text} ${tiny_status}${reset_text}" 1130 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE =============================================${reset_text}" 1131 | moveXOffset; printf " %-10s${dns_heatmap}%-16s${reset_text} %-8s${ftl_heatmap}%-10s${reset_text}${clear_line}\n" "DNS:" "${dns_status}" "FTL:" "${ftl_status}" 1132 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ===============================================${reset_text}" 1133 | moveXOffset; printf " %-10s%-29s${clear_line}\n" "Blocking:" "${domains_being_blocked} domains" 1134 | moveXOffset; printf " %-10s[%-30s] %-5s${clear_line}\n" "Pi-holed:" "${ads_blocked_bar}" "${ads_percentage_today}%" 1135 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Pi-holed:" "${ads_blocked_today} out of ${dns_queries_today}" 1136 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Latest:" "${latest_blocked}" 1137 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Ad:" "${top_blocked}" 1138 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK =============================================${reset_text}" 1139 | moveXOffset; printf " %-10s%-16s %-8s%-16s${clear_line}\n" "Hostname:" "${full_hostname}" "IP: " "${pi_ip4_addr}" 1140 | moveXOffset; printf " %-10s%-16s %-4s%-7s %-4s%-5s${clear_line}\n" "Interfce:" "${iface_name}" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}" 1141 | moveXOffset; printf " %-10s%-16s %-8s${dnssec_heatmap}%-16s${reset_text}${clear_line}\n" "DNS:" "${dns_information}" "DNSSEC:" "${dnssec_status}" 1142 | moveXOffset; printf " %-10s%s${clear_line}\n" "IPv6:" "${pi_ip6_addr}" 1143 | moveXOffset; printf " %-10s%-15s%-4s${dhcp_range_heatmap}%-36s${reset_text}${clear_line}\n" "DHCP:" "${dhcp_check_box}" "Rng" "${dhcp_range}" 1144 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM ==============================================${reset_text}" 1145 | moveXOffset; printf " %-10s%-29s${clear_line}\n" "Uptime:" "${system_uptime}" 1146 | 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}" 1147 | 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}%" 1148 | elif [ "$1" = "regular" ] || [ "$1" = "slim" ]; then 1149 | # slim is a screen with at least 60 columns and exactly 21 lines 1150 | # regular is a screen at least 60x22 (columns x lines) 1151 | if [ "$1" = "slim" ]; then 1152 | moveXOffset; printf "%s${clear_line}\n" "${padd_text}${dim_text}slim${reset_text} ${version_info}${reset_text}" 1153 | moveXOffset; printf "%s${clear_line}\n" " PADD ${padd_version_heatmap}${padd_version}${reset_text} ${full_status}${reset_text}" 1154 | moveXOffset; printf "%s${clear_line}\n" "" 1155 | else 1156 | moveXOffset; printf "%s${clear_line}\n" "${padd_logo_1}" 1157 | moveXOffset; printf "%s${clear_line}\n" "${padd_logo_2}${version_info}${reset_text}" 1158 | moveXOffset; printf "%s${clear_line}\n" "${padd_logo_3}PADD ${padd_version_heatmap}${padd_version}${reset_text} ${full_status}${reset_text}" 1159 | moveXOffset; printf "%s${clear_line}\n" "" 1160 | fi 1161 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}PI-HOLE ====================================================${reset_text}" 1162 | moveXOffset; printf " %-10s${dns_heatmap}%-19s${reset_text} %-10s${ftl_heatmap}%-19s${reset_text}${clear_line}\n" "DNS:" "${dns_status}" "FTL:" "${ftl_status}" 1163 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ======================================================${reset_text}" 1164 | moveXOffset; printf " %-10s%-49s${clear_line}\n" "Blocking:" "${domains_being_blocked} domains" 1165 | moveXOffset; printf " %-10s[%-40s] %-5s${clear_line}\n" "Pi-holed:" "${ads_blocked_bar}" "${ads_percentage_today}%" 1166 | moveXOffset; printf " %-10s%-49s${clear_line}\n" "Pi-holed:" "${ads_blocked_today} out of ${dns_queries_today} queries" 1167 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Latest:" "${latest_blocked}" 1168 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Ad:" "${top_blocked}" 1169 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ====================================================${reset_text}" 1170 | moveXOffset; printf " %-10s%-15s %-4s%-17s${clear_line}\n" "Hostname:" "${full_hostname}" "IP:" "${pi_ip4_addr}" 1171 | moveXOffset; printf " %-10s%-15s %-4s%-17s%-4s%s${clear_line}\n" "Interfce:" "${iface_name}" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}" 1172 | moveXOffset; printf " %-10s%s${clear_line}\n" "IPv6:" "${pi_ip6_addr}" 1173 | moveXOffset; printf " %-10s%-15s %-10s${dnssec_heatmap}%-19s${reset_text}${clear_line}\n" "DNS:" "${dns_information}" "DNSSEC:" "${dnssec_status}" 1174 | moveXOffset; printf " %-10s%-16s%-6s${dhcp_range_heatmap}%-36s${reset_text}${clear_line}\n" "DHCP:" "${dhcp_check_box}" "Range" "${dhcp_range}" 1175 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =====================================================${reset_text}" 1176 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Uptime:" "${system_uptime}" 1177 | 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}" 1178 | 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}%" 1179 | else # ${padd_size} = mega 1180 | # mega is a screen with at least 80 columns and 26 lines 1181 | moveXOffset; printf "%s${clear_line}\n" "${padd_logo_retro_1}" 1182 | moveXOffset; printf "%s${clear_line}\n" "${padd_logo_retro_2} ${version_info}, PADD ${padd_version_heatmap}${padd_version}${reset_text}" 1183 | moveXOffset; printf "%s${clear_line}\n" "${padd_logo_retro_3} ${dns_check_box} DNS ${ftl_check_box} FTL ${mega_status}${reset_text}" 1184 | moveXOffset; printf "%s${clear_line}\n" "" 1185 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}STATS ==========================================================================${reset_text}" 1186 | moveXOffset; printf " %-10s%-19s %-10s[%-40s] %-5s${clear_line}\n" "Blocking:" "${domains_being_blocked} domains" "Piholed:" "${ads_blocked_bar}" "${ads_percentage_today}%" 1187 | moveXOffset; printf " %-10s%-30s%-29s${clear_line}\n" "Clients:" "${clients}" " ${ads_blocked_today} out of ${dns_queries_today} queries" 1188 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Latest:" "${latest_blocked}" 1189 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Ad:" "${top_blocked}" 1190 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Dmn:" "${top_domain}" 1191 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Top Clnt:" "${top_client}" 1192 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}FTL ============================================================================${reset_text}" 1193 | moveXOffset; printf " %-10s%-9s %-10s%-9s %-10s%-9s${clear_line}\n" "PID:" "${ftlPID}" "CPU Use:" "${ftl_cpu}" "Mem. Use:" "${ftl_mem_percentage}" 1194 | moveXOffset; printf " %-10s%-69s${clear_line}\n" "DNSCache:" "${cache_inserts} insertions, ${cache_evictions} deletions, ${cache_size} total entries" 1195 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}NETWORK ========================================================================${reset_text}" 1196 | moveXOffset; printf " %-10s%-19s${clear_line}\n" "Hostname:" "${full_hostname}" 1197 | moveXOffset; printf " %-10s%-15s %-4s%-9s %-4s%-9s${clear_line}\n" "Interfce:" "${iface_name}" "TX:" "${tx_bytes}" "RX:" "${rx_bytes}" 1198 | moveXOffset; printf " %-6s%-19s %-10s%-29s${clear_line}\n" "IPv4:" "${pi_ip4_addr}" "IPv6:" "${pi_ip6_addr}" 1199 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}DNS ==========================DHCP==============================================${reset_text}" 1200 | moveXOffset; printf " %-10s%-19s %-6s${dhcp_heatmap}%-19s${reset_text}${clear_line}\n" "Servers:" "${dns_information}" "DHCP:" "${dhcp_status}" 1201 | 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}" 1202 | 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}" 1203 | moveXOffset; printf "%s${clear_line}\n" "${bold_text}SYSTEM =========================================================================${reset_text}" 1204 | moveXOffset; printf " %-10s%-39s${clear_line}\n" "Device:" "${sys_model}" 1205 | moveXOffset; printf " %-10s%-39s %-10s[${memory_heatmap}%-10s${reset_text}] %-6s${clear_line}\n" "Uptime:" "${system_uptime}" "Memory:" "${memory_bar}" "${memory_percent}%" 1206 | 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}%" 1207 | fi 1208 | 1209 | # Clear to end of screen (below the drawn dashboard) 1210 | # https://vt100.net/docs/vt510-rm/ED.html 1211 | printf '\e[0J' 1212 | } 1213 | 1214 | ############################################# HELPERS ############################################## 1215 | 1216 | # Provides a color based on a provided percentage 1217 | # takes in one or two parameters 1218 | HeatmapGenerator () { 1219 | # if one number is provided, just use that percentage to figure out the colors 1220 | if [ -z "$2" ]; then 1221 | load=$(printf "%.0f" "$1") 1222 | # if two numbers are provided, do some math to make a percentage to figure out the colors 1223 | else 1224 | load=$(printf "%.0f" "$(echo "$1 $2" | awk '{print ($1 / $2) * 100}')") 1225 | fi 1226 | 1227 | # Color logic 1228 | # |<- green ->| yellow | red -> 1229 | # 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 1230 | if [ "${load}" -lt 75 ]; then 1231 | out=${green_text} 1232 | elif [ "${load}" -lt 90 ]; then 1233 | out=${yellow_text} 1234 | else 1235 | out=${red_text} 1236 | fi 1237 | 1238 | echo "$out" 1239 | } 1240 | 1241 | # Provides a "bar graph" 1242 | # takes in two or three parameters 1243 | # $1: percentage filled 1244 | # $2: max length of the bar 1245 | # $3: colored flag, if "color" backfill with color 1246 | BarGenerator() { 1247 | # number of filled in cells in the bar 1248 | barNumber=$(printf %.f "$(echo "$1 $2" | awk '{print ($1 / 100) * $2}')") 1249 | frontFill=$(for i in $(seq "$barNumber"); do printf "%b" "■"; done) 1250 | 1251 | # remaining "unfilled" cells in the bar 1252 | backfillNumber=$(($2-barNumber)) 1253 | 1254 | # if the filled in cells is less than the max length of the bar, fill it 1255 | if [ "$barNumber" -lt "$2" ]; then 1256 | # if the bar should be colored 1257 | if [ "$3" = "color" ]; then 1258 | # fill the rest in color 1259 | backFill=$(for i in $(seq $backfillNumber); do printf "%b" "■"; done) 1260 | out="${red_text}${frontFill}${green_text}${backFill}${reset_text}" 1261 | # else, it shouldn't be colored in 1262 | else 1263 | # fill the rest with "space" 1264 | backFill=$(for i in $(seq $backfillNumber); do printf "%b" "·"; done) 1265 | out="${frontFill}${reset_text}${backFill}" 1266 | fi 1267 | # else, fill it all the way 1268 | else 1269 | out=$(for i in $(seq "$2"); do printf "%b" "■"; done) 1270 | fi 1271 | 1272 | echo "$out" 1273 | } 1274 | 1275 | # Checks the size of the screen and sets the value of ${padd_data}_size 1276 | SizeChecker(){ 1277 | # adding a tiny delay here to to give the kernel a bit time to 1278 | # report new sizes correctly after a terminal resize 1279 | # this reduces "flickering" of GenerateSizeDependendOutput() items 1280 | # after a terminal re-size 1281 | sleep 0.1 1282 | console_width=$(tput cols) 1283 | console_height=$(tput lines) 1284 | 1285 | # Mega 1286 | if [ "$console_width" -ge "80" ] && [ "$console_height" -ge "26" ]; then 1287 | padd_size="mega" 1288 | width=80 1289 | height=26 1290 | # Below Mega. Gives you Regular. 1291 | elif [ "$console_width" -ge "60" ] && [ "$console_height" -ge "22" ]; then 1292 | padd_size="regular" 1293 | width=60 1294 | height=22 1295 | # Below Regular. Gives you Slim. 1296 | elif [ "$console_width" -ge "60" ] && [ "$console_height" -ge "21" ]; then 1297 | padd_size="slim" 1298 | width=60 1299 | height=21 1300 | # Below Slim. Gives you Tiny. 1301 | elif [ "$console_width" -ge "53" ] && [ "$console_height" -ge "20" ]; then 1302 | padd_size="tiny" 1303 | width=53 1304 | height=20 1305 | # Below Tiny. Gives you Mini. 1306 | elif [ "$console_width" -ge "40" ] && [ "$console_height" -ge "18" ]; then 1307 | padd_size="mini" 1308 | width=40 1309 | height=18 1310 | # Below Mini. Gives you Micro. 1311 | elif [ "$console_width" -ge "30" ] && [ "$console_height" -ge "16" ]; then 1312 | padd_size="micro" 1313 | width=30 1314 | height=16 1315 | # Below Micro, Gives you Nano. 1316 | elif [ "$console_width" -ge "24" ] && [ "$console_height" -ge "12" ]; then 1317 | padd_size="nano" 1318 | width=24 1319 | height=12 1320 | # Below Nano. Gives you Pico. 1321 | elif [ "$console_width" -ge "20" ] && [ "$console_height" -ge "10" ]; then 1322 | padd_size="pico" 1323 | width=20 1324 | height=10 1325 | # Below Pico. Gives you nothing... 1326 | else 1327 | padd_size="ants" 1328 | fi 1329 | 1330 | # Center the output (default position) 1331 | xOffset="$(( (console_width - width) / 2 ))" 1332 | yOffset="$(( (console_height - height) / 2 ))" 1333 | 1334 | # If the user sets an offset option, use it. 1335 | if [ -n "$xOffOrig" ]; then 1336 | xOffset=$xOffOrig 1337 | 1338 | # Limit the offset to avoid breaks 1339 | xMaxOffset=$((console_width - width)) 1340 | if [ "$xOffset" -gt "$xMaxOffset" ]; then 1341 | xOffset="$xMaxOffset" 1342 | fi 1343 | fi 1344 | if [ -n "$yOffOrig" ]; then 1345 | yOffset=$yOffOrig 1346 | 1347 | # Limit the offset to avoid breaks 1348 | yMaxOffset=$((console_height - height)) 1349 | if [ "$yOffset" -gt "$yMaxOffset" ]; then 1350 | yOffset="$yMaxOffset" 1351 | fi 1352 | fi 1353 | } 1354 | 1355 | # converts a given version string e.g. v3.7.1 to 3007001000 to allow for easier comparison of multi digit version numbers 1356 | # credits https://apple.stackexchange.com/a/123408 1357 | VersionConverter() { 1358 | echo "$@" | tr -d '[:alpha:]' | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; 1359 | } 1360 | 1361 | moveYOffset(){ 1362 | # moves the cursor yOffset-times down 1363 | # https://vt100.net/docs/vt510-rm/CUD.html 1364 | # this needs to be guarded, because if the amount is 0, it is adjusted to 1 1365 | # https://terminalguide.namepad.de/seq/csi_cb/ 1366 | 1367 | if [ "${yOffset}" -gt 0 ]; then 1368 | printf '\e[%sB' "${yOffset}" 1369 | fi 1370 | } 1371 | 1372 | moveXOffset(){ 1373 | # moves the cursor xOffset-times to the right 1374 | # https://vt100.net/docs/vt510-rm/CUF.html 1375 | # this needs to be guarded, because if the amount is 0, it is adjusted to 1 1376 | # https://terminalguide.namepad.de/seq/csi_cb/ 1377 | 1378 | if [ "${xOffset}" -gt 0 ]; then 1379 | printf '\e[%sC' "${xOffset}" 1380 | fi 1381 | } 1382 | 1383 | # Remove undesired strings from sys_model variable - used in GetSystemInformation() function 1384 | filterModel() { 1385 | FILTERLIST="To be filled by O.E.M.|Not Applicable|System Product Name|System Version|Undefined|Default string|Not Specified|Type1ProductConfigId|INVALID|All Series|�" 1386 | 1387 | # Description: 1388 | # `-v` : set $FILTERLIST into a variable called `list` 1389 | # `gsub()` : replace all list items (ignoring case) with an empty string, deleting them 1390 | # `{$1=$1}1`: remove all extra spaces. The last "1" evaluates as true, printing the result 1391 | echo "$1" | awk -v list="$FILTERLIST" '{IGNORECASE=1; gsub(list,"")}; {$1=$1}1' 1392 | } 1393 | 1394 | # Truncates a given string and appends three '...' 1395 | # takes two parameters 1396 | # $1: string to truncate 1397 | # $2: max length of the string 1398 | truncateString() { 1399 | local truncatedString length shorted 1400 | 1401 | length=${#1} 1402 | shorted=$(($2-3)) # shorten max allowed length by 3 to make room for the dots 1403 | if [ "${length}" -gt "$2" ]; then 1404 | # if length of the string is larger then the specified max length 1405 | # cut every char from the string exceeding length $shorted and add three dots 1406 | truncatedString=$(echo "$1" | cut -c1-$shorted)"..." 1407 | echo "${truncatedString}" 1408 | else 1409 | echo "$1" 1410 | fi 1411 | } 1412 | 1413 | # Converts seconds to days, hours, minutes 1414 | # https://unix.stackexchange.com/a/338844 1415 | convertUptime() { 1416 | # shellcheck disable=SC2016 1417 | eval "echo $(date -ud "@$1" +'$((%s/3600/24)) days, %H hours, %M minutes')" 1418 | } 1419 | 1420 | secretRead() { 1421 | 1422 | # POSIX compliant function to read user-input and 1423 | # mask every character entered by (*) 1424 | # 1425 | # This is challenging, because in POSIX, `read` does not support 1426 | # `-s` option (suppressing the input) or 1427 | # `-n` option (reading n chars) 1428 | 1429 | 1430 | # This workaround changes the terminal characteristics to not echo input and later resets this option 1431 | # credits https://stackoverflow.com/a/4316765 1432 | # showing asterisk instead of password 1433 | # https://stackoverflow.com/a/24600839 1434 | # https://unix.stackexchange.com/a/464963 1435 | 1436 | stty -echo # do not echo user input 1437 | stty -icanon min 1 time 0 # disable canonical mode https://man7.org/linux/man-pages/man3/termios.3.html 1438 | 1439 | unset password 1440 | unset key 1441 | unset charcount 1442 | charcount=0 1443 | while key=$(dd ibs=1 count=1 2>/dev/null); do #read one byte of input 1444 | if [ "${key}" = "$(printf '\0' | tr -d '\0')" ] ; then 1445 | # Enter - accept password 1446 | break 1447 | fi 1448 | if [ "${key}" = "$(printf '\177')" ] ; then 1449 | # Backspace 1450 | if [ $charcount -gt 0 ] ; then 1451 | charcount=$((charcount-1)) 1452 | printf '\b \b' 1453 | password="${password%?}" 1454 | fi 1455 | else 1456 | # any other character 1457 | charcount=$((charcount+1)) 1458 | printf '*' 1459 | password="$password$key" 1460 | fi 1461 | done 1462 | 1463 | # restore original terminal settings 1464 | stty "${stty_orig}" 1465 | } 1466 | 1467 | check_dependencies() { 1468 | # Check for required dependencies 1469 | if ! command -v curl >/dev/null 2>&1; then 1470 | printf "%b" "${check_box_bad} Error!\n 'curl' is missing but required.\n" 1471 | exit 1 1472 | fi 1473 | 1474 | if ! command -v jq >/dev/null 2>&1; then 1475 | printf "%b" "${check_box_bad} Error!\n 'jq' is missing but required.\n" 1476 | exit 1 1477 | fi 1478 | } 1479 | 1480 | ########################################## MAIN FUNCTIONS ########################################## 1481 | 1482 | OutputJSON() { 1483 | # Hiding the cursor. 1484 | # https://vt100.net/docs/vt510-rm/DECTCEM.html 1485 | printf '\e[?25l' 1486 | # Traps for graceful shutdown 1487 | # https://unix.stackexchange.com/a/681201 1488 | trap CleanExit EXIT 1489 | trap sig_cleanup INT QUIT TERM 1490 | # Save current terminal settings (needed for later restore after password prompt) 1491 | stty_orig=$(stty -g) 1492 | 1493 | # Test if the authentication endpoint is available 1494 | TestAPIAvailability 1495 | # Authenticate with the FTL server 1496 | printf "%b" "Establishing connection with FTL...\n" 1497 | LoginAPI 1498 | 1499 | GetPADDData 1500 | GetSummaryInformation 1501 | 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}}" 1502 | } 1503 | 1504 | ShowVersion() { 1505 | # Hiding the cursor. 1506 | # https://vt100.net/docs/vt510-rm/DECTCEM.html 1507 | printf '\e[?25l' 1508 | # Traps for graceful shutdown 1509 | # https://unix.stackexchange.com/a/681201 1510 | trap CleanExit EXIT 1511 | trap sig_cleanup INT QUIT TERM 1512 | 1513 | # Save current terminal settings (needed for later restore after password prompt) 1514 | stty_orig=$(stty -g) 1515 | 1516 | # Test if the authentication endpoint is available 1517 | TestAPIAvailability 1518 | # Authenticate with the FTL server 1519 | printf "%b" "Establishing connection with FTL...\n" 1520 | LoginAPI 1521 | 1522 | GetPADDData 1523 | GetVersionInformation 1524 | GetPADDInformation 1525 | if [ -z "${padd_version_latest}" ]; then 1526 | padd_version_latest="N/A" 1527 | fi 1528 | if [ ! "${DOCKER_VERSION}" = "null" ]; then 1529 | # Check for latest Docker version 1530 | 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})" 1531 | version_info="Docker ${docker_version_heatmap}${DOCKER_VERSION}${reset_text}" 1532 | else 1533 | printf "%s${clear_line}\n" "PADD version is ${padd_version_heatmap}${padd_version}${reset_text} (Latest: ${padd_version_latest})" 1534 | fi 1535 | } 1536 | 1537 | StartupRoutine(){ 1538 | 1539 | if [ "$1" = "ants" ]; then 1540 | # If the screen is too small from the beginning, exit 1541 | printf "%b" "${check_box_bad} Error!\n PADD isn't\n for ants!\n" 1542 | exit 1 1543 | fi 1544 | 1545 | # Clear the screen and move cursor to (0,0). 1546 | # This mimics the 'clear' command. 1547 | # https://vt100.net/docs/vt510-rm/ED.html 1548 | # https://vt100.net/docs/vt510-rm/CUP.html 1549 | # E3 extension `\e[3J` to clear the scrollback buffer see 'man clear' 1550 | printf '\e[H\e[2J\e[3J' 1551 | 1552 | # adds the y-offset 1553 | moveYOffset 1554 | 1555 | if [ "$1" = "pico" ] || [ "$1" = "nano" ] || [ "$1" = "micro" ]; then 1556 | moveXOffset; PrintLogo "$1" 1557 | moveXOffset; printf "%b" "START-UP ===========\n" 1558 | 1559 | # Test if the authentication endpoint is available 1560 | TestAPIAvailability 1561 | 1562 | # Authenticate with the FTL server 1563 | moveXOffset; printf "%b" "Establishing connection with FTL...\n" 1564 | LoginAPI 1565 | 1566 | moveXOffset; printf "%b" "Starting PADD...\n" 1567 | 1568 | moveXOffset; printf "%b" " [■·········] 10%\r" 1569 | 1570 | # Request PADD data 1571 | GetPADDData 1572 | 1573 | # Check for updates 1574 | moveXOffset; printf "%b" " [■■········] 20%\r" 1575 | moveXOffset; printf "%b" " [■■■·······] 30%\r" 1576 | 1577 | # Get our information for the first time 1578 | moveXOffset; printf "%b" " [■■■■······] 40%\r" 1579 | GetVersionInformation 1580 | moveXOffset; printf "%b" " [■■■■■·····] 50%\r" 1581 | GetSummaryInformation 1582 | moveXOffset; printf "%b" " [■■■■■■····] 60%\r" 1583 | GetPiholeInformation 1584 | moveXOffset; printf "%b" " [■■■■■■■···] 70%\r" 1585 | GetNetworkInformation 1586 | moveXOffset; printf "%b" " [■■■■■■■■··] 80%\r" 1587 | GetSystemInformation 1588 | moveXOffset; printf "%b" " [■■■■■■■■■·] 90%\r" 1589 | GetPADDInformation 1590 | moveXOffset; printf "%b" " [■■■■■■■■■■] 100%\n" 1591 | 1592 | elif [ "$1" = "mini" ]; then 1593 | moveXOffset; PrintLogo "$1" 1594 | moveXOffset; echo "START UP =====================" 1595 | # Test if the authentication endpoint is available 1596 | TestAPIAvailability 1597 | # Authenticate with the FTL server 1598 | moveXOffset; printf "%b" "Establishing connection with FTL...\n" 1599 | LoginAPI 1600 | 1601 | # Request PADD data 1602 | moveXOffset; echo "- Requesting PADD information..." 1603 | GetPADDData 1604 | 1605 | # Get our information for the first time 1606 | moveXOffset; echo "- Gathering version info." 1607 | GetVersionInformation 1608 | moveXOffset; echo "- Gathering system info." 1609 | GetSystemInformation 1610 | moveXOffset; echo "- Gathering CPU/DNS info." 1611 | GetPiholeInformation 1612 | GetSummaryInformation 1613 | moveXOffset; echo "- Gathering network info." 1614 | GetNetworkInformation 1615 | GetPADDInformation 1616 | if [ ! "${DOCKER_VERSION}" = "null" ]; then 1617 | moveXOffset; echo " - Docker Tag ${DOCKER_VERSION}" 1618 | else 1619 | moveXOffset; echo " - Core $CORE_VERSION, Web $WEB_VERSION" 1620 | moveXOffset; echo " - FTL $FTL_VERSION, PADD ${padd_version}" 1621 | fi 1622 | 1623 | else 1624 | moveXOffset; printf "%b" "${padd_logo_retro_1}\n" 1625 | moveXOffset; printf "%b" "${padd_logo_retro_2}Pi-hole® Ad Detection Display\n" 1626 | moveXOffset; printf "%b" "${padd_logo_retro_3}A client for Pi-hole\n\n" 1627 | if [ "$1" = "tiny" ]; then 1628 | moveXOffset; echo "START UP ============================================" 1629 | else 1630 | moveXOffset; echo "START UP ===================================================" 1631 | fi 1632 | 1633 | # Test if the authentication endpoint is available 1634 | TestAPIAvailability 1635 | 1636 | # Authenticate with the FTL server 1637 | moveXOffset; printf "%b" "Establishing connection with FTL...\n" 1638 | LoginAPI 1639 | 1640 | # Request PADD data 1641 | moveXOffset; echo "- Requesting PADD information..." 1642 | GetPADDData 1643 | 1644 | # Get our information for the first time 1645 | moveXOffset; echo "- Gathering version information..." 1646 | GetVersionInformation 1647 | moveXOffset; echo "- Gathering system information..." 1648 | GetSystemInformation 1649 | moveXOffset; echo "- Gathering CPU/DNS information..." 1650 | GetSummaryInformation 1651 | GetPiholeInformation 1652 | moveXOffset; echo "- Gathering network information..." 1653 | GetNetworkInformation 1654 | 1655 | GetPADDInformation 1656 | if [ ! "${DOCKER_VERSION}" = "null" ]; then 1657 | moveXOffset; echo " - Docker Tag ${DOCKER_VERSION}" 1658 | else 1659 | moveXOffset; echo " - Pi-hole Core $CORE_VERSION" 1660 | moveXOffset; echo " - Web Admin $WEB_VERSION" 1661 | moveXOffset; echo " - FTL $FTL_VERSION" 1662 | moveXOffset; echo " - PADD ${padd_version}" 1663 | fi 1664 | fi 1665 | 1666 | moveXOffset; printf "%s" "- Starting in " 1667 | for i in 3 2 1 1668 | do 1669 | printf "%s..." "$i" 1670 | sleep 1 1671 | done 1672 | } 1673 | 1674 | NormalPADD() { 1675 | 1676 | # Trap the window resize signal (handle window resize events) 1677 | trap 'TerminalResize' WINCH 1678 | 1679 | 1680 | # Clear the screen once on startup to remove overflow from the startup routine 1681 | printf '\033[2J' 1682 | 1683 | while true; do 1684 | 1685 | # Generate output that depends on the terminal size 1686 | # e.g. Heatmap and barchart 1687 | GenerateSizeDependendOutput ${padd_size} 1688 | 1689 | # Sets the message displayed in the "status field" depending on the set flags 1690 | SetStatusMessage 1691 | 1692 | # Output everything to the screen 1693 | PrintDashboard ${padd_size} 1694 | 1695 | # Sleep for 5 seconds 1696 | # sending sleep in the background and wait for it 1697 | # this way the TerminalResize trap can kill the sleep 1698 | # and force a instant re-draw of the dashboard 1699 | # https://stackoverflow.com/questions/32041674/linux-how-to-kill-sleep 1700 | # 1701 | # saving the PID of the background sleep process to kill it on exit and resize 1702 | sleep 5 & 1703 | sleepPID=$! 1704 | wait $! 1705 | 1706 | # Start getting our information for next round 1707 | now=$(date +%s) 1708 | 1709 | # check if a new authentication is required (e.g. after connection to FTL has re-established) 1710 | # GetFTLData() will return a 401 if a 401 http status code is returned 1711 | # as $password should be set already, PADD should automatically re-authenticate 1712 | authenthication_required=$(GetFTLData "info/ftl") 1713 | if [ "${authenthication_required}" = 401 ]; then 1714 | Authenticate 1715 | fi 1716 | 1717 | # Request PADD data after 30 seconds or if the connection was lost 1718 | if [ $((now - LastCheckFullInformation)) -ge 30 ] || [ "${connection_down_flag}" = true ] ; then 1719 | GetPADDData 1720 | LastCheckFullInformation="${now}" 1721 | else 1722 | # Request only a subset of the data 1723 | GetPADDData "?full=false" 1724 | fi 1725 | 1726 | connection_down_flag=false 1727 | # If the connection was lost, set connection_down_flag 1728 | if [ "${padd_data}" = "000" ]; then 1729 | connection_down_flag=true 1730 | GetSystemInformation 1731 | GetSummaryInformation 1732 | GetPiholeInformation 1733 | GetNetworkInformation 1734 | GetVersionInformation 1735 | # set flag to update network information in the next loop in case the connection is re-established 1736 | get_network_information_requried=true 1737 | else 1738 | # Get uptime, CPU load, temp, etc. every 5 seconds 1739 | GetSystemInformation 1740 | GetSummaryInformation 1741 | GetPiholeInformation 1742 | 1743 | if [ $((now - LastCheckNetworkInformation)) -ge 30 ] || [ "${get_network_information_requried}" = true ]; then 1744 | GetNetworkInformation 1745 | GetVersionInformation 1746 | LastCheckNetworkInformation="${now}" 1747 | get_network_information_requried=false 1748 | fi 1749 | 1750 | # Get PADD version information every 24hours 1751 | if [ $((now - LastCheckPADDInformation)) -ge 86400 ]; then 1752 | GetPADDInformation 1753 | LastCheckPADDInformation="${now}" 1754 | fi 1755 | fi 1756 | 1757 | done 1758 | } 1759 | 1760 | Update() { 1761 | # source version file to check if $DOCKER_VERSION is set 1762 | . /etc/pihole/versions 1763 | 1764 | if [ -n "${DOCKER_VERSION}" ]; then 1765 | echo "${check_box_info} Update is not supported for Docker" 1766 | exit 1 1767 | fi 1768 | 1769 | GetPADDInformation 1770 | 1771 | if [ "${padd_out_of_date_flag}" = "true" ]; then 1772 | echo "${check_box_info} Updating PADD from ${padd_version} to ${padd_version_latest}" 1773 | 1774 | padd_script_path=$(realpath "$0") 1775 | 1776 | echo "${check_box_info} Downloading PADD update ..." 1777 | 1778 | if curl --connect-timeout 5 -sSL https://install.padd.sh -o "${padd_script_path}" > /dev/null 2>&1; then 1779 | echo "${check_box_good} ... done. Restart PADD for the update to take effect" 1780 | else 1781 | echo "${check_box_bad} Cannot download PADD update" 1782 | echo "${check_box_info} Go to https://install.padd.sh to download the update manually" 1783 | exit 1 1784 | fi 1785 | else 1786 | echo "${check_box_good} You are already using the latest PADD version ${padd_version}" 1787 | fi 1788 | 1789 | exit 0 1790 | } 1791 | 1792 | DisplayHelp() { 1793 | cat << EOM 1794 | 1795 | ::: PADD displays stats about your Pi-hole! 1796 | ::: 1797 | ::: 1798 | ::: Options: 1799 | ::: --xoff [num] set the x-offset, reference is the upper left corner, disables auto-centering 1800 | ::: --yoff [num] set the y-offset, reference is the upper left corner, disables auto-centering 1801 | ::: 1802 | ::: --server domain or IP of your Pi-hole (default: localhost) 1803 | ::: --secret your Pi-hole's password, required to access the API 1804 | ::: --2fa <2fa> your Pi-hole's 2FA code, if 2FA is enabled 1805 | ::: -j, --json output stats as JSON formatted string and exit 1806 | ::: -u, --update update to the latest version 1807 | ::: -v, --version show PADD version info 1808 | ::: -h, --help display this help text 1809 | 1810 | EOM 1811 | } 1812 | 1813 | # Called on signals INT QUIT TERM 1814 | sig_cleanup() { 1815 | # save error code (130 for SIGINT, 143 for SIGTERM, 131 for SIGQUIT) 1816 | err=$? 1817 | 1818 | # some shells will call EXIT after the INT signal 1819 | # causing EXIT trap to be executed, so we trap EXIT after INT 1820 | trap '' EXIT 1821 | 1822 | (exit $err) # execute in a subshell just to pass $? to CleanExit() 1823 | CleanExit 1824 | } 1825 | 1826 | # Called on signal EXIT, or indirectly on INT QUIT TERM 1827 | CleanExit() { 1828 | # save the return code of the script 1829 | err=$? 1830 | 1831 | # reset trap for all signals to not interrupt clean_tempfiles() on any next signal 1832 | trap '' EXIT INT QUIT TERM 1833 | 1834 | # restore terminal settings if they have been changed (e.g. user canceled script while at password input prompt) 1835 | if [ "$(stty -g)" != "${stty_orig}" ]; then 1836 | stty "${stty_orig}" 1837 | fi 1838 | 1839 | # Show the cursor 1840 | # https://vt100.net/docs/vt510-rm/DECTCEM.html 1841 | printf '\e[?25h' 1842 | 1843 | # if background sleep is running, kill it 1844 | # http://mywiki.wooledge.org/SignalTrap#When_is_the_signal_handled.3F 1845 | kill "{$sleepPID}" > /dev/null 2>&1 1846 | 1847 | # Delete session from FTL server 1848 | DeleteSession 1849 | exit $err # exit the script with saved $? 1850 | } 1851 | 1852 | TerminalResize(){ 1853 | # if a terminal resize is trapped, check the new terminal size and 1854 | # kill the sleep function within NormalPADD() to trigger redrawing 1855 | # of the Dashboard 1856 | SizeChecker 1857 | 1858 | # Clear the screen and move cursor to (0,0). 1859 | # This mimics the 'clear' command. 1860 | # https://vt100.net/docs/vt510-rm/ED.html 1861 | # https://vt100.net/docs/vt510-rm/CUP.html 1862 | # E3 extension `\e[3J` to clear the scrollback buffer (see 'man clear') 1863 | 1864 | printf '\e[H\e[2J\e[3J' 1865 | 1866 | kill "{$sleepPID}" > /dev/null 2>&1 1867 | } 1868 | 1869 | main(){ 1870 | 1871 | check_dependencies 1872 | 1873 | # Hiding the cursor. 1874 | # https://vt100.net/docs/vt510-rm/DECTCEM.html 1875 | printf '\e[?25l' 1876 | 1877 | # Traps for graceful shutdown 1878 | # https://unix.stackexchange.com/a/681201 1879 | trap CleanExit EXIT 1880 | trap sig_cleanup INT QUIT TERM 1881 | 1882 | # Save current terminal settings (needed for later restore after password prompt) 1883 | stty_orig=$(stty -g) 1884 | 1885 | 1886 | SizeChecker 1887 | 1888 | StartupRoutine ${padd_size} 1889 | 1890 | # Run PADD 1891 | NormalPADD 1892 | } 1893 | 1894 | # Process all options (if present) 1895 | while [ "$#" -gt 0 ]; do 1896 | case "$1" in 1897 | "-j" | "--json" ) xOffset=0; OutputJSON; exit 0;; 1898 | "-u" | "--update" ) Update;; 1899 | "-h" | "--help" ) DisplayHelp; exit 0;; 1900 | "-v" | "--version" ) xOffset=0; ShowVersion; exit 0;; 1901 | "--xoff" ) xOffset="$2"; xOffOrig="$2"; shift;; 1902 | "--yoff" ) yOffset="$2"; yOffOrig="$2"; shift;; 1903 | "--server" ) SERVER="$2"; shift;; 1904 | "--secret" ) password="$2"; shift;; 1905 | "--2fa" ) totp="$2"; shift;; 1906 | * ) DisplayHelp; exit 1;; 1907 | esac 1908 | shift 1909 | done 1910 | 1911 | main 1912 | --------------------------------------------------------------------------------