Python coding

Ping The Thing

last updated: 2022-01-18

Ping The Thing is a simple Python script that checks if all your devices are connected to the net and is sending a mail if not.

Intro

I have an IP camera that looses sometimes the connection with my internal net. My NAS does not restart itself after a power loss. My MQTT server and other servers (e.g. openHAB) as well as some sensors are important and must run to not loose data and to guarantee that all controls in the house are functioning.

If there is a problem with one of these devices I would like to know as soon as possible. I'm more the e-mail type and not the GSM type :). So I need a warning mail if something is amiss.

Fortunately a simple Python script running on a Raspi (e.g. the pi-hole raspi) or other server can help.

ping<em>the</em>thing

Installing the mail program

I want to be able to send mails from different python scripts to myself, so we will install a message transfer agent (MTA) on the Raspi. Normally I would use sSMTP, but unfortunately this package has been orphaned since 2019-03-19. As stated in the debian wiki msmtp can be used as an alternative.

Msmtp can send mails from MUAs (mail user agents) like Mutt or Emacs. To use a standard MTA interface like I was used with sSMTP we install the msmtp-mta package. It provides a /usr/sbin/sendmail symlink to msmtp. We also need mailutils for the mail program. For attachments (if needed) we can install the mpack package,

Install everything with:

    sudo apt install msmtp msmtp-mta mailutils mpack

Next we create a config file with the name .msmtprc in home (/home/pi or ~). It is a hidden file and we need to make it readable.

    nano ..msmtprc    

Now copy the following to the file and change your account settings. You can add as many accounts as you wish.

    # Set default values for all following accounts.
    defaults
    auth           on
    tls            on
    tls_trust_file /etc/ssl/certs/ca-certificates.crt
    logfile        ~/.msmtp.log

    # Gmail
    account        gmail
    host           smtp.gmail.com
    port           587
    from           username@gmail.com
    user           username
    password       plain-text-password

    # Set a default account
    account default : gmail

Save with Ctrl+o and make the file readable:

    chmod 600 ~/.msmtprc

Now we can test with the following line:

    echo "Hello world email body" | mail -s "Test Subject" username@gmail.com

Sure it is better to not use a cleartext password in a file. You can use a passwordmanager or gpg as described on this page: https://wiki.archlinux.org/title/msmtp.

The python script

    #!/usr/bin/env python3
    # -*- coding:utf-8 -*-

    """ ping_the_thing.py from weigu.lu """

    __version__ = "0.1.0"
    __author__ = "Guy WEILER weigu.lu"
    __copyright__ = "Copyright 2022, weigu.lu"    
    __license__ = "GPL"
    __maintainer__ = "Guy WEILER"
    __email__ = "weigu@weigu.lu"
    __status__ = "Production" # "Prototype", "Development", or "Production"

    from time import sleep, time
    import datetime as dt
    import subprocess as sp
    import sys

    # Mail address to send the mail to
    RECEIVER = 'mymail@address' # changer this !! :)

    # add here your Ip addresses and a meaningfull name of the thing
    WAIT_BETWEEN_CHECKS = 5 # in minutes

    # add here your Ip addresses and a meaningfull name of the thing
    IP_DICT =  {'192.168.1.50':'myNAS',
                '192.168.1.99':'myImportantServer',
                '192.168.1.100':'myIoTSensor',
                '192.168.1.101':'myMy',
               }

    ##############################################################################

    def ipcheck(ip_dict):
        """ Ping the IP addresses from the dictionary and return a list
        if they are up or down"""
        ip_up_list = []
        for ip_addr in ip_dict.keys():
            status, _ = sp.getstatusoutput("ping -c1 -w2 " + ip_addr)
            if status == 0:
                ip_up_list.append('up')
            else:
                ip_up_list.append('down')
        return ip_up_list

    def send_mail(subject, message):
        """ Send the message string to the RECEIVER mail address. """
        try:
            retcode = sp.call('echo ' + message + '| mail -s ' + subject +
                              ' ' + RECEIVER, shell=True)
            if retcode < 0:
                print("Child was terminated by signal", -retcode, file=sys.stderr)
            else:
                print("Mail was sent! Child returned", retcode, file=sys.stderr)
        except OSError as error:
            print("Error sending mail: Execution failed:", error, file=sys.stderr)

    def send_warning_mail(res_dict, mail_flag_list):
        """ Cook the mail. """
        key_list = list(res_dict.keys())
        val_list = list(res_dict.values())
        for i, _ in enumerate(val_list):
            if val_list[i] == 'down':
                if mail_flag_list[i] != 'true':
                    subject = 'WARNING_Mail'
                    message =  key_list[i] + ' is down! '
                    print(message, end = '')
                    send_mail(subject, message)
                    mail_flag_list[i] = 'true'
        return mail_flag_list

    def create_mail_flag_list():
        """ This list is needed to make shure a warning mail
            is only sent once a day"""
        mail_flag_list = []
        key_list = list(IP_DICT.keys())
        for _ in key_list:
            mail_flag_list.append('false')
        return mail_flag_list

    ##############################################################################

    def main():
        """ main """
        print("\"ping_the_thing.py\" started at ",end='')
        print(dt.datetime.now().isoformat('T', 'seconds'))
        mail_flag_list = create_mail_flag_list()

        while True:
            now_time = dt.datetime.now().time()
            if dt.time(0,9,50) <= now_time <= dt.time(0,9,55):
                mail_flag_list = create_mail_flag_list()
                sleep(5)
            now_sec = time() # in seconds
            if int(now_sec%(60*WAIT_BETWEEN_CHECKS)) == 0:
                ip_up_list = ipcheck(IP_DICT)
                res_dict = dict(zip(IP_DICT.values(), ip_up_list))
                mail_flag_list = send_warning_mail(res_dict, mail_flag_list)
                print('!',end = '')
            sleep(1)

    ##############################################################################

    if __name__ == '__main__':
        main()

Run the program at boot

We add the following line to the user crontab file. First we start the editor with:

    crontab -e

Choose nano as simplest editor and add the following line to the file:

    @reboot sleep 60 && python3 /home/pi/ping_the_thing.py

Save with Ctrl+o and reboot your server.

We wait for 1 minute so that the network is up, before running the script.

We do not use the system crontab (/etc/crontab) running as root, but a user crontab (with crontab -e) that runs under the user pi. As root we get an "Process exited with a non-zero status" error because the mail program is setup for the user pi. The user crontabs are in /var/spool/cron.

Downloads

Interesting links: