diff --git a/code/python/package/README.txt b/code/python/package/README.txt index e45a6a7..fd0c185 100644 --- a/code/python/package/README.txt +++ b/code/python/package/README.txt @@ -7,25 +7,25 @@ This directory contains the installation scripts and the PiModules(R) Python pac Installation ------------ -First you need to install a couple of Python dependencies. Install python-pip like this: +First you need to install a couple of Python dependencies. Install python3-pip like this: - sudo apt-get install python-pip + sudo apt-get install python3-pip Then install both `jinja2` and `xmltodict` like this: - sudo pip install jinja2 - sudo pip install xmltodict + sudo pip3 install jinja2 + sudo pip3 install xmltodict -Both of the above libraries are used in the sending of email alerts by the file-safe shutdown +Both of the above libraries are used in the sending of email alerts by the file-safe shutdown daemon. Now install the pimodules package: - sudo python setup.py install + sudo python3 setup.py install You can record what files are installed by the above process by running it like this: - sudo python setup.py install --record + sudo python3 setup.py install --record Where will be a text file containing one line per file installed by the process. @@ -68,6 +68,3 @@ This file. * setup.py Installation file, see `README.md` in the directory above this one. - - - diff --git a/code/python/package/pimodules/alerts.py b/code/python/package/pimodules/alerts.py index 7b47015..fb85721 100755 --- a/code/python/package/pimodules/alerts.py +++ b/code/python/package/pimodules/alerts.py @@ -8,78 +8,78 @@ def get_host_name(): - try: - command = "uname -n" - process = subprocess.Popen(command.split(), stdout=subprocess.PIPE) - output = process.communicate()[0] - return output - except: - return None + try: + command = "uname -n" + process = subprocess.Popen(command.split(), stdout=subprocess.PIPE) + output = process.communicate()[0] + return output + except: + return None def get_ip_address(): - try: - command = "hostname -I" - process = subprocess.Popen(command.split(), stdout=subprocess.PIPE) - output = process.communicate()[0] - return output - except: - return None - - -def sendEmail( emailserver, username, port, security, fromAddr, toAddr, b64Password, msgSubjectTemplate, msgBodyTemplate): - try: - port = int(port) - except ValueError: - port = 0 - - try: - now = datetime.datetime.now() - host = get_host_name() - ipaddress = get_ip_address() - except: - raise - - password = base64.b64decode(b64Password) - - tVars= { 'now' : now, 'host' : host, 'ipaddress' : ipaddress } - - try: - templateLoader = jinja2.FileSystemLoader(searchpath="/") - templateEnv = jinja2.Environment( loader=templateLoader ) - - subjectTemplate = templateEnv.get_template(msgSubjectTemplate) - bodyTemplate = templateEnv.get_template(msgBodyTemplate) - - msgSubject = subjectTemplate.render(tVars) - msgBody = bodyTemplate.render(tVars) - - except: - raise - - # Writing the message - msg = MIMEText(msgBody) - msg['Subject'] = msgSubject - msg['From'] = fromAddr - msg['To'] = toAddr - - # Sending the message - try: - if port: - serverstring = format("%s:%d" % (emailserver, port)) - else: - serverstring = emailserver - - if security.lower() in ['ssl']: - server = smtplib.SMTP_SSL(serverstring) - else: - server = smtplib.SMTP(serverstring) - server.starttls() - - server.login(username, password) - server.sendmail(fromAddr, [toAddr], msg.as_string()) - server.quit() - except: - raise - - return True + try: + command = "hostname -I" + process = subprocess.Popen(command.split(), stdout=subprocess.PIPE) + output = process.communicate()[0] + return output + except: + return None + + +def sendEmail(emailserver, username, port, security, fromAddr, toAddr, b64Password, msgSubjectTemplate, msgBodyTemplate): + try: + port = int(port) + except ValueError: + port = 0 + + try: + now = datetime.datetime.now() + host = get_host_name().decode('utf-8') + ipaddress = get_ip_address() + except: + raise + + password = base64.b64decode(b64Password).decode('utf-8') + + tVars = {'now': now, 'host': host, 'ipaddress': ipaddress} + + try: + templateLoader = jinja2.FileSystemLoader(searchpath="/") + templateEnv = jinja2.Environment(loader=templateLoader) + + subjectTemplate = templateEnv.get_template(msgSubjectTemplate) + bodyTemplate = templateEnv.get_template(msgBodyTemplate) + + msgSubject = subjectTemplate.render(tVars) + msgBody = bodyTemplate.render(tVars) + + except: + raise + + # Writing the message + msg = MIMEText(msgBody) + msg['Subject'] = msgSubject + msg['From'] = fromAddr + msg['To'] = toAddr + + # Sending the message + try: + if port: + serverstring = format("%s:%d" % (emailserver, port)) + else: + serverstring = emailserver + + if security.lower() in ['ssl']: + server = smtplib.SMTP_SSL(serverstring) + else: + server = smtplib.SMTP(serverstring) + server.starttls() + + server.login(username, password) + server.sendmail(fromAddr, [toAddr], msg.as_string()) + server.quit() + except: + raise + + return True diff --git a/code/python/package/pimodules/configuration.py b/code/python/package/pimodules/configuration.py index 9440d23..ecac5fc 100755 --- a/code/python/package/pimodules/configuration.py +++ b/code/python/package/pimodules/configuration.py @@ -41,9 +41,7 @@ def write_config_xml(xmlfile, dict): with open(xmlfile, "wt") as fo: xmltodict.unparse(dict, fo, pretty=True) except IOError as e: - print "Error writing XML file: ", e + print("Error writing XML file: "+e) return False return True - - diff --git a/code/python/package/pimodules/daemon.py b/code/python/package/pimodules/daemon.py index 16193a2..43474f1 100644 --- a/code/python/package/pimodules/daemon.py +++ b/code/python/package/pimodules/daemon.py @@ -1,84 +1,61 @@ +"""Generic linux daemon base class for python 3.x.""" + +import sys, os, time, atexit, signal + +class Daemon: + """A generic daemon class. + + Usage: subclass the daemon class and override the run() method.""" + + def __init__(self, pidfile): self.pidfile = pidfile -import sys -import os -import time -import atexit -import logging -import logging.handlers - -from signal import SIGTERM - - - -class Daemon(object): - """ - A forking daemon - - Usage: subclass the Daemon class and override the run() method - """ - def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): - self.log = logging.getLogger(__name__) - self.log.setLevel(logging.DEBUG) - handler = logging.handlers.SysLogHandler(address = '/dev/log') - formatter = logging.Formatter('%(module)s[%(process)s]: <%(levelname)s>: %(message)s') - handler.setFormatter(formatter) - self.log.addHandler(handler) - - self.stdin = stdin - self.stdout = stdout - self.stderr = stderr - - self.pidfile = pidfile - def daemonize(self): - """ - do the UNIX double-fork magic, see Stevens' "Advanced - Programming in the UNIX Environment" for details (ISBN 0201563177) - http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 - """ + """Deamonize class. UNIX double fork mechanism.""" - try: - pid = os.fork() + try: + pid = os.fork() if pid > 0: # exit first parent - sys.exit(0) - except OSError, e: - sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #1 failed: {0}\n'.format(err)) sys.exit(1) - + # decouple from parent environment - os.chdir("/") - os.setsid() - os.umask(0) - + os.chdir('/') + os.setsid() + os.umask(0) + # do second fork - try: - pid = os.fork() + try: + pid = os.fork() if pid > 0: + # exit from second parent - sys.exit(0) - except OSError, e: - sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) - sys.exit(1) - + sys.exit(0) + except OSError as err: + sys.stderr.write('fork #2 failed: {0}\n'.format(err)) + sys.exit(1) + # redirect standard file descriptors sys.stdout.flush() sys.stderr.flush() - - si = file(self.stdin, 'r') - so = file(self.stdout, 'a+') - se = file(self.stderr, 'a+', 0) + si = open(os.devnull, 'r') + so = open(os.devnull, 'a+') + se = open(os.devnull, 'a+') os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) - + # write pidfile - atexit.register(self.deletePidFile) + atexit.register(self.delpid) + pid = str(os.getpid()) - file(self.pidfile,'w+').write("%s\n" % pid) - - def deletePidFile(self): + with open(self.pidfile,'w+') as f: + f.write(pid + '\n') + + def delpid(self): os.remove(self.pidfile) def start(self, daemonize=True): @@ -90,76 +67,61 @@ def start(self, daemonize=True): if daemonize: - # Check for a pidfile to see if the daemon is already running + # Check for a pidfile to see if the daemon already runs try: - pf = file(self.pidfile,'r') - pid = int(pf.read().strip()) - pf.close() + with open(self.pidfile,'r') as pf: + + pid = int(pf.read().strip()) except IOError: pid = None - - if daemonize: + if pid: - message = "pidfile %s already exist. Daemon already running?\n" - sys.stderr.write(message % self.pidfile) + message = "pidfile {0} already exist. " + \ + "Daemon already running?\n" + sys.stderr.write(message.format(self.pidfile)) sys.exit(1) - - # Start the daemon - if daemonize: - self.daemonize() + # Start the daemon + self.daemonize() self.run() def stop(self): - """ - Stop the daemon - """ - + """Stop the daemon.""" if self.daemon: # Get the pid from the pidfile try: - pf = file(self.pidfile,'r') - pid = int(pf.read().strip()) - pf.close() + with open(self.pidfile,'r') as pf: + pid = int(pf.read().strip()) except IOError: - print 'could not get pid' pid = None - - if self.daemon: + if not pid: - message = "pidfile %s does not exist. Daemon not running?\n" - sys.stderr.write(message % self.pidfile) + message = "pidfile {0} does not exist. " + \ + "Daemon not running?\n" + sys.stderr.write(message.format(self.pidfile)) return # not an error in a restart - if self.daemon: - # Try killing the daemon process + # Try killing the daemon process try: while 1: - os.kill(pid, SIGTERM) + os.kill(pid, signal.SIGTERM) time.sleep(0.1) - except OSError, err: - err = str(err) - if err.find("No such process") > 0: + except OSError as err: + e = str(err.args) + if e.find("No such process") > 0: if os.path.exists(self.pidfile): os.remove(self.pidfile) else: - print str(err) + print (str(err.args)) sys.exit(1) def restart(self): - """ - Restart the daemon - """ + """Restart the daemon.""" self.stop() self.start() def run(self): - """ - You should override this method when you subclass Daemon. It will be called after the process has been - daemonized by start() or restart(). - """ - - raise NotImplementedError - - + """You should override this method when you subclass Daemon. + It will be called after the process has been daemonized by + start() or restart().""" diff --git a/code/python/package/pyproject.toml b/code/python/package/pyproject.toml new file mode 100644 index 0000000..96781c1 --- /dev/null +++ b/code/python/package/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pimodules" +version = "0.1.dev0" +description = "UPSPIco file-safe shutdown daemon" +readme = "README.txt" +license = "GPL-3.0-or-later" +authors = [{ name = "Mike Ray", email = "mike.ray@btinternet.com" }] +classifiers = [ + "Development Status :: 5 - Testing/Beta", + "Intended Audience :: PiModules UPS PiCo Product developers and partners (change to customers when published)", + "Programming Language :: Python >= 2.7", + "Topic :: PiModules(R)", + "Topic :: UPS PIco support daemon", + "Operating System :: Linux (Raspbian)", +] +dependencies = ["xmltodict", "jinja2"] + +[project.urls] +homepage = "http://pimodules.com" + +[tool.setuptools] +packages = ["pimodules"] diff --git a/code/python/package/setup.py b/code/python/package/setup.py index 93daa29..9f37232 100755 --- a/code/python/package/setup.py +++ b/code/python/package/setup.py @@ -4,30 +4,30 @@ A package which contains code to support PiModules products of various kinds. """ -classifiers = """\ -Development Status :: 5 - Testing/Beta -Intended Audience :: PiModules Product Developers -License :: GNU General Public License Version 3 -Programming Language :: Python >= 2.7 -Topic :: PiModules(R) -Topic :: PiModules(R) Products Python Package -Operating System :: Linux (Raspbian) -""" +# classifiers = """\ +# Development Status :: 5 - Testing/Beta +# Intended Audience :: PiModules Product Developers +# License :: GNU General Public License Version 3 +# Programming Language :: Python >= 2.7 +# Topic :: PiModules(R) +# Topic :: PiModules(R) Products Python Package +# Operating System :: Linux (Raspbian) +# """ -from distutils.core import setup +from setuptools import setup doclines = __doc__.split("\n") - -setup(name='pimodules', - version='0.1dev', - description=doclines[0], - long_description = "\n".join(doclines[2:]), - license='GPL3', - author='Mike Ray', - author_email='mike.ray@btinternet.com', - url='http://pimodules.com', - platforms=['POSIX'], - classifiers = filter(None, classifiers.split("\n")), - packages=['pimodules'], - ) +setup() +# setup(name='pimodules', +# version='0.1.dev0', +# description=doclines[0], +# long_description = "\n".join(doclines[2:]), +# license='GPL3', +# author='Mike Ray', +# author_email='mike.ray@btinternet.com', +# url='http://pimodules.com', +# platforms=['POSIX'], +# classifiers = list(filter(None, classifiers.split("\n"))), +# packages=['pimodules'], +# ) diff --git a/code/python/upspico/picofssd/QUICKSTART.md b/code/python/upspico/picofssd/QUICKSTART.md index 4fd47fa..3cff429 100644 --- a/code/python/upspico/picofssd/QUICKSTART.md +++ b/code/python/upspico/picofssd/QUICKSTART.md @@ -2,21 +2,22 @@ Installation ------------ -See README.txt in this directory for dependancies. +See README.txt in this directory for dependencies. - sudo python setup.py install + sudo python3 setup.py install cd ../../package - sudo python setup.py install - sudo update-rc.d picofssd defaults - sudo update-rc.d picofssd enable + sudo python3 setup.py install + sudo systemctl deamon-reload + sudo systemctl picofssd.service start + sudo systemctl picofssd.service enable After dependancies have been installed and the Pi has been rebooted the daemon should start. XML Configuration File ---------------------- -The file-safe shutdown daemon requires an XML configuration file to run. It controls the email -alert settings that the daemon uses to know how to send an email when it detects a shutdown has been +The file-safe shutdown daemon requires an XML configuration file to run. It controls the email +alert settings that the daemon uses to know how to send an email when it detects a shutdown has been requested by the UPS PIco. A script is installed by the installation process which can be used to create this XML file. @@ -43,12 +44,12 @@ Or it can create this file directly like this: sudo picofssdxmlconfig -d -o /etc/pimodules/picofssd/picofssd.xml -You can also read from an existing XML configuration file and create a new one, keeping some of the +You can also read from an existing XML configuration file and create a new one, keeping some of the settings from the old file and only changing your choice of settings. To do this: picofssdxmlconfig -i oldfile.xml -o newfile.xml -Settings will be read from the old file and presented for you to either keep or replace and write +Settings will be read from the old file and presented for you to either keep or replace and write forward to the new file. Here is an explanation of the settings you will be asked to enter. @@ -77,7 +78,7 @@ The email address to which the alert email should be sent. The password required by the email server. -The password is stored on the filesystem encrypted and decryypted by the daemon when it sends an +The password is stored on the filesystem encrypted and decryypted by the daemon when it sends an email. * Security method @@ -94,7 +95,5 @@ A file name of the `jinja2` template used by the daemon to generate the text for Absolute path to a `jinja2` template used by the daemon to generate the email message body. -In most cases you will be asked to confirm each entry and asked to enter each twice to make sure you +In most cases you will be asked to confirm each entry and asked to enter each twice to make sure you entered them correctly. - - diff --git a/code/python/upspico/picofssd/picofssd.egg-info/requires.txt b/code/python/upspico/picofssd/picofssd.egg-info/requires.txt new file mode 100644 index 0000000..1845c58 --- /dev/null +++ b/code/python/upspico/picofssd/picofssd.egg-info/requires.txt @@ -0,0 +1,4 @@ +fake-rpi +xmltodict +jinja2 +rpi-lgpio diff --git a/code/python/upspico/picofssd/pyproject.toml b/code/python/upspico/picofssd/pyproject.toml new file mode 100644 index 0000000..417211a --- /dev/null +++ b/code/python/upspico/picofssd/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +dependencies = [ + 'fake-rpi; platform_system != "Linux"', + 'xmltodict', + 'jinja2', + 'rpi-lgpio; platform_system == "Linux"', +] +name = "picofssd" +version = "0.2.dev1" +description = "UPSPIco file-safe shutdown daemon" +readme = "README.txt" +license = "GPL-3.0-or-later" +authors = [{ name = "Mike Ray", email = "mike.ray@btinternet.com" }] +classifiers = [ + "Development Status :: 5 - Testing/Beta", + "Intended Audience :: PiModules UPS PiCo Product developers and partners (change to customers when published)", + "Programming Language :: Python >= 2.7", + "Topic :: PiModules(R)", + "Topic :: UPS PIco support daemon", + "Operating System :: Linux (Raspbian)", +] + +[project.urls] +homepage = "http://pimodules.com" + +[tool.setuptools] +packages = ["scripts"] + +[tool.setuptools.data-files] +"." = ["scripts/picofssd.py", "scripts/picofssdxmlconfig"] + +[project.scripts] +picofssd = "scripts.picofssd:main" diff --git a/code/python/upspico/picofssd/scripts/__init__.py b/code/python/upspico/picofssd/scripts/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/code/python/upspico/picofssd/scripts/picofssd b/code/python/upspico/picofssd/scripts/picofssd deleted file mode 100644 index a117518..0000000 --- a/code/python/upspico/picofssd/scripts/picofssd +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/python - -""" - -PiModules(R) UPS PIco file-safe shutdown daemon. - -""" - -import sys -import os -import signal -import atexit -import time -import logging -import logging.handlers -import argparse -import xmltodict -import socket -import RPi.GPIO as GPIO - -from pimodules.daemon import Daemon -from pimodules.alerts import sendEmail -from pimodules import configuration - -CLOCK_PIN = 27 -PULSE_PIN = 22 -BOUNCE_TIME = 30 - - -class fssd(Daemon): - def __init__(self, pidfile, xmlconfig, loglevel=logging.NOTSET): - Daemon.__init__(self, pidfile) - self.loglevel = loglevel - self.log = logging.getLogger(__name__) - self.log.setLevel(self.loglevel) - handler = logging.handlers.SysLogHandler(address = '/dev/log') - formatter = logging.Formatter('%(module)s[%(process)s]: <%(levelname)s>: %(message)s') - handler.setFormatter(formatter) - self.log.addHandler(handler) - - try: - self.xmlconfig = xmlconfig - with open(self.xmlconfig, 'rt') as fi: - self.config = xmltodict.parse(fi) - except IOError as e: - self.log.warning("Failed to load XML config file, loading defaults. Alerts will be disabled") - self.config = xmltodict.parse(configuration.DEFAULT_FSSD_XML_CONFIG) - - self.config = self.config['root']['fssd:config']['fssd:alerts']['fssd:email'] - self.config['fssd:enabled'] = (self.config['fssd:enabled'] == 'True') - - signal.signal(signal.SIGTERM, self.sigcatch) - - self.counter = 0 - - # first interrupt on isr pin will start pulse high - self.sqwave = True - - def setup(self): - """ - GPIO initialisation - - Note: This cannot go in the __init__ method because the unix double-fork in the generic daemon code - mucks up the initialisation of the GPIO system. - - So it is called in the over-ridden run method. - """ - - GPIO.setmode(GPIO.BCM) - GPIO.setwarnings(False) - GPIO.setup(CLOCK_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) - GPIO.setup(PULSE_PIN, GPIO.OUT, initial=self.sqwave) - GPIO.add_event_detect(CLOCK_PIN, GPIO.FALLING, callback=self.isr, bouncetime=BOUNCE_TIME) - - def isr(self, channel): - """ - GPIO interrupt service routine - """ - - # This test is here because the user *might* have another HAT plugged in or another circuit that produces a - # falling-edge signal on another GPIO pin. - if channel != CLOCK_PIN: - return - - # we can get the state of a pin with GPIO.input even when it is currently configured as an output - self.sqwave = not GPIO.input(PULSE_PIN) - - # set pulse pin low before changing it to input to look for shutdown signal - GPIO.output(PULSE_PIN, False) - GPIO.setup(PULSE_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) - if not GPIO.input(PULSE_PIN): - # pin is low, this is shutdown signal from pico - self.counter += 1 - self.log.warning("Lost power supply, Pi will shutdown") - self.alert_email() - time.sleep(2) - os.system('/sbin/shutdown -h now') - else: - self.counter = 0 - - # change pulse pin back to output with flipped state - GPIO.setup(PULSE_PIN, GPIO.OUT, initial=self.sqwave) - - def sigcatch(self, signum, frame): - """ - Signal handler - """ - - if signum == signal.SIGTERM: - sys.exit(0) - - def cleanup(self): - """ - GPIO cleanup - """ - - self.log.debug("Cleanup") - self.log.info("Stopped") - GPIO.cleanup() - - def alert_email(self): - #emailserver, username, port, security, fromAddr, toAddr, b64Password, msgSubjectTemplate, msgBodyTemplate - try: - if not self.config['fssd:enabled']: - self.log.debug("Email alert is disabled") - return True - - sendEmail(self.config['fssd:server'], self.config['fssd:username'], self.config['fssd:port'], self.config['fssd:security'], - self.config['fssd:sender-email-address'], self.config['fssd:recipient-email-address'], - self.config['fssd:sender-password'], self.config['fssd:subject-template'], - self.config['fssd:body-template']) - except socket.error as e: - self.log.error(format("Exception in alert_email: %d, %s" % (e.errno, e.strerror))) - except: - self.log.error("Unexpected error in alert_email:", sys.exc_info()[0]) - - - def run(self): - """ - Super-class overloaded run method. - """ - - self.log.info("Started") - self.log.debug(self.config['fssd:enabled']) - self.log.debug(self.config['fssd:server']) - self.log.debug(self.config['fssd:username']) - self.log.debug(self.config['fssd:sender-email-address']) - self.log.debug(self.config['fssd:recipient-email-address']) - - # register function to cleanup at exit - atexit.register(self.cleanup) - - self.setup() - - while True: - time.sleep(5) - - - -# parse the command-line -parser = argparse.ArgumentParser() -parser.add_argument('-l', '--log-level', help="Log level, 'info' or 'debug'", default='info', choices=['info', 'debug']) -parser.add_argument("-x", "--xml-config", help="XML config file", default='picofssd.xml', required=True) -group = parser.add_mutually_exclusive_group(required=True) -group.add_argument("-d", "--debug", help="Keep in the foreground, do not daemonize", action="store_true", default=False) -group.add_argument("-p", "--pid-file", help="PID file") -args = parser.parse_args() - -sd = fssd(args.pid_file, args.xml_config, {'info':logging.INFO, 'debug':logging.DEBUG}[args.log_level]) - -# the argument to the start method is opposite of debug -sd.start(not args.debug) - diff --git a/code/python/upspico/picofssd/scripts/picofssd.py b/code/python/upspico/picofssd/scripts/picofssd.py new file mode 100755 index 0000000..abaa9f6 --- /dev/null +++ b/code/python/upspico/picofssd/scripts/picofssd.py @@ -0,0 +1,191 @@ + +#!/usr/bin/python + +""" + +PiModules(R) UPS PIco file-safe shutdown daemon. + +""" +import sys # noqa +print(sys.platform) +if sys.platform!="linux": # noqa + print("Faking libraries") + import fake_rpi # noqa + sys.modules['RPi'] = fake_rpi.RPi # noqa Fake RPi + sys.modules['RPi.GPIO'] = fake_rpi.RPi.GPIO # noqa Fake GPIO + sys.modules['smbus'] = fake_rpi.smbus # noqa Fake smbus (I2C) + +import socket +import xmltodict +import argparse +import logging.handlers +import logging +import time +import atexit +import signal +import os +import RPi.GPIO as GPIO +from pimodules.daemon import Daemon +from pimodules.alerts import sendEmail +from pimodules import configuration + + +CLOCK_PIN = 27 +PULSE_PIN = 22 +BOUNCE_TIME = 30 + + +class fssd(Daemon): + def __init__(self, pidfile, xmlconfig, loglevel=logging.NOTSET): + Daemon.__init__(self, pidfile) + self.loglevel = loglevel + self.log = logging.getLogger(__name__) + self.log.setLevel(self.loglevel) + if sys.platform == "Linux": + handler = logging.handlers.SysLogHandler(address='/dev/log') + else: + handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter( + '%(module)s[%(process)s]: <%(levelname)s>: %(message)s') + handler.setFormatter(formatter) + self.log.addHandler(handler) + + try: + self.xmlconfig = xmlconfig + with open(self.xmlconfig, 'rt', encoding='utf-8') as fi: + self.config = xmltodict.parse(fi.read()) + except IOError as e: + self.log.warning( + "Failed to load XML config file, loading defaults. Alerts will be disabled") + self.config = xmltodict.parse( + configuration.DEFAULT_FSSD_XML_CONFIG) + + self.config = self.config['root']['fssd:config']['fssd:alerts']['fssd:email'] + self.config['fssd:enabled'] = (self.config['fssd:enabled'] == 'True') + + signal.signal(signal.SIGTERM, self.sigcatch) + + self.counter = 0 + + # first interrupt on isr pin will start pulse high + self.sqwave = True + + def setup(self): + """ + GPIO initialisation + + Note: This cannot go in the __init__ method because the unix double-fork in the generic daemon code + mucks up the initialisation of the GPIO system. + + So it is called in the over-ridden run method. + """ + + GPIO.setmode(GPIO.BCM) + GPIO.setwarnings(False) + GPIO.setup(CLOCK_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) + GPIO.setup(PULSE_PIN, GPIO.OUT, initial=self.sqwave) + GPIO.add_event_detect(CLOCK_PIN, GPIO.FALLING, + callback=self.isr, bouncetime=BOUNCE_TIME) + + def isr(self, channel): + """ + GPIO interrupt service routine + """ + # This test is here because the user *might* have another HAT plugged in or another circuit that produces a + # falling-edge signal on another GPIO pin. + if channel != CLOCK_PIN: + return + + # we can get the state of a pin with GPIO.input even when it is currently configured as an output + self.sqwave = not GPIO.input(PULSE_PIN) + + # set pulse pin low before changing it to input to look for shutdown signal + GPIO.output(PULSE_PIN, False) + GPIO.setup(PULSE_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP) + if not GPIO.input(PULSE_PIN): + # pin is low, this is shutdown signal from pico + self.counter += 1 + self.log.warning("Lost power supply, Pi will shutdown") + self.alert_email() + time.sleep(2) + os.system('/sbin/shutdown -h now') + else: + self.counter = 0 + + # change pulse pin back to output with flipped state + GPIO.setup(PULSE_PIN, GPIO.OUT, initial=self.sqwave) + + def sigcatch(self, signum, frame): + """ + Signal handler + """ + + if signum == signal.SIGTERM: + sys.exit(0) + + def cleanup(self): + """ + GPIO cleanup + """ + + self.log.debug("Cleanup") + self.log.info("Stopped") + GPIO.cleanup() + + def alert_email(self): + # emailserver, username, port, security, fromAddr, toAddr, b64Password, msgSubjectTemplate, msgBodyTemplate + try: + if not self.config['fssd:enabled']: + self.log.debug("Email alert is disabled") + return True + + sendEmail(self.config['fssd:server'], self.config['fssd:username'], self.config['fssd:port'], self.config['fssd:security'], + self.config['fssd:sender-email-address'], self.config['fssd:recipient-email-address'], + self.config['fssd:sender-password'], self.config['fssd:subject-template'], + self.config['fssd:body-template']) + except socket.error as e: + self.log.error( + format("Exception in alert_email: %d, %s" % (e.errno, e.strerror))) + except: + self.log.error("Unexpected error in alert_email:", + sys.exc_info()[0]) + + def run(self): + """ + Super-class overloaded run method. + """ + + self.log.info("Started") + self.log.debug(self.config['fssd:enabled']) + self.log.debug(self.config['fssd:server']) + self.log.debug(self.config['fssd:username']) + self.log.debug(self.config['fssd:sender-email-address']) + self.log.debug(self.config['fssd:recipient-email-address']) + + # register function to cleanup at exit + atexit.register(self.cleanup) + + self.setup() + + while True: + time.sleep(5) + + +# parse the command-line +parser = argparse.ArgumentParser() +parser.add_argument('-l', '--log-level', help="Log level, 'info' or 'debug'", + default='info', choices=['info', 'debug']) +parser.add_argument("-x", "--xml-config", help="XML config file", + default='picofssd.xml', required=True) +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument("-d", "--debug", help="Keep in the foreground, do not daemonize", + action="store_true", default=False) +group.add_argument("-p", "--pid-file", help="PID file") +args = parser.parse_args() +print(args) +print(args.xml_config) +sd = fssd(args.pid_file, args.xml_config, { + 'info': logging.INFO, 'debug': logging.DEBUG}[args.log_level]) + +# the argument to the start method is opposite of debug +sd.start(not args.debug) diff --git a/code/python/upspico/picofssd/scripts/picofssdxmlconfig b/code/python/upspico/picofssd/scripts/picofssdxmlconfig index 7a84be4..077ed6b 100755 --- a/code/python/upspico/picofssd/scripts/picofssdxmlconfig +++ b/code/python/upspico/picofssd/scripts/picofssdxmlconfig @@ -1,321 +1,318 @@ -#!/usr/bin/python - -import argparse -import xmltodict -import getpass -import base64 -from pimodules import configuration - - -def get_yesno(help, prompt): - """ - Accepts either 'y' or 'n' at the command-line - """ - print help - yn = None - while yn not in ['y','n']: - yn = raw_input(prompt) or 'x' - if yn not in ['y','n']: - print "Please answer 'y' or 'n'" - - return yn == 'y' - -def is_correct(prompt, strval): - """ - Asks the user whether a value entered is correct - """ - yn = None - while yn not in ['y','n']: - yn = raw_input(format("You entered %s for the %s, is this correct? " % (strval, prompt))).lower() - if yn not in ['y','n']: - print "Please answer 'y' or 'n'" - - return yn == 'y' - -def enter_string(help, prompt, default=None, twice=True): - """ - Accepts a string value at the command line, asks the user to confirm whether it is the correct - value and optionally does the accept twice and compares the two occurrences - """ - print help - default = default or '' - savedefault = default - while True: - if default: - pt = format("Enter the value for the %s [ %s ]: " % (prompt, default)) - str1 = raw_input(pt) or default - else: - pt = format("Enter the value for the %s: " % prompt) - str1 = raw_input(pt) - if not str1: - print "You cannot enter a zero-length string" - continue - - if is_correct(prompt, str1): - if not twice: - return str1 - else: - if str1 != default: - default = '' - - if default: - pt = format("Re-enter the value for the %s (%s): " % (prompt, default)) - str2 = raw_input(pt) or default - else: - pt = format("Re-enter the value for the %s: " % prompt) - str2 = raw_input(pt) - - if str1 == str2: - break - else: - print "Values do not match" - - return str2 - - - -def enter_integer(help, prompt, default=None, twice=True): - """ - Accepts a string at the command-line. Checks whether the string is a valid - integer, optionally accepting it twice and comparing the two strings to make sure - they are the same - """ - print help - default = default or '' - savedefault = default - while True: - if default: - pt = format("Enter the value for the %s [ %s ]: " % (prompt, default)) - str1 = raw_input(pt) or default - else: - pt = format("Enter the value for the %s: " % prompt) - str1 = raw_input(pt) - if not str1: - print "You cannot enter a zero-length string" - continue - try: - integer = int(str1) - except ValueError: - print "You must enter a valid integer" - continue - except TypeError: - print "You must enter a valid integer" - continue - - if is_correct(prompt, str1): - if not twice: - return str1 - else: - if str1 != default: - default = '' - - if default: - pt = format("Re-enter the value for the %s (%s): " % (prompt, default)) - str2 = raw_input(pt) or default - else: - pt = format("Re-enter the value for the %s: " % prompt) - str2 = raw_input(pt) - - if str1 == str2: - break - else: - print "Values do not match" - - return str2 - - -def double_enter_password(help, prompt): - """ - Accepts a password entered at the command-line and checks both instances - are the same - """ - print help - while True: - pt = format("Enter the value for the %s: " % prompt) - str1 = getpass.getpass(pt) - pt = format("Re-enter the value for the %s: " % prompt) - str2 = getpass.getpass(pt) - - if str1 == str2: - break - else: - print "Values do not match" - - return str2 - - -def get_choice(help, prompt, choices, show=False): - """ - Accepts a string at the command-line. Choice entered must be one of the choices in the list. Optionally shows the choices - available in the prompt - """ - print help - choice = '' - if show: - prompt += '(' + ','.join(choices) + ')' - - while choice.lower() not in choices: - try: - choice = raw_input(prompt).lower() - except EOFError: - raise KeyboardInterrupt - - return choice - - -#-- main program - -parser = argparse.ArgumentParser() - -group = parser.add_mutually_exclusive_group(required=True) -group.add_argument("-d", "--default", - help="Start from a fresh XML configuration, do not initialize values from existing XML file", - action="store_true", default=False) -group.add_argument("-i", "--input-xml", - help="Read initial configuration values from an existing file") -parser.add_argument("-o", "--output-xml", - help="Output XML configuration file", - default='picofssd.xml', required=True) - -args = parser.parse_args() - -if args.default: - configdict = configuration.read_config_xml(configuration.DEFAULT_FSSD_XML_CONFIG, True) -else: - configdict = configuration.read_config_xml(args.input_xml, False) - - -configsubdict = configdict['root']['fssd:config']['fssd:alerts']['fssd:email'] - -help = """ - -Follow the prompts and enter strings for the XML configuration which drives -the UPS PIco file-safe shutdown daemon. - -Default values read from the previous XML configuration are shown in square brackets. Just press enter -to accept a default value. - -""" - -enabled = get_yesno(help, "Do you want to enable the email alert? ('y', 'n'): ") -configsubdict['fssd:enabled'] = enabled - - -help = """ - -Email server. This is usually something like 'mail.domain.com', where 'domain' reflects the company providing the -email account. - -""" - -server = configsubdict['fssd:server'] -server = enter_string(help, "mail server", default=server,twice=True) -configsubdict['fssd:server'] = server - -help = """ - -Email account user name. This is the user name used to log into the email server. This is usually -the full email address of the account holder. - -""" - -username = configsubdict['fssd:username'] -username = enter_string(help, "email user name (this is usually the full email address)", default=username, twice=True) -configsubdict['fssd:username'] = username - - -help = """ - -Email address of the originator (sender) of the email. This is usually the same as the user name if -the full email address is used as the server's log in string given above. - -""" - -sender = configsubdict['fssd:sender-email-address'] -sender = enter_string(help, "Sender's email address", default=sender, twice=True) -configsubdict['fssd:sender-email-address'] = sender - -help = """ - -The password for the email server and the email account. - -""" - -password = double_enter_password(help, "password") -configsubdict['fssd:sender-password'] = base64.b64encode(password) - - -help = """ - -The recipient's email address to which the daemon should send an email alert. - -""" - -recipient = configsubdict['fssd:recipient-email-address'] -recipient = enter_string(help, "destination email address", default=recipient, twice=True) -configsubdict['fssd:recipient-email-address'] = recipient - - -help = """ - -The security method employed by the email SMTP server. Permitted methods are SSL (secure server layer) -or TLS (transport level security). - -""" - -security = get_choice(help, "Enter server security method ", ['ssl', 'tls'], True) -configsubdict['fssd:security'] = security - -help = """ - -The port number the email server uses. If security was 'SSL', this is probably 465. - -The value entered must be a valid integer. - -""" - -strport = configsubdict['fssd:port'] -try: - port = int(strport) -except ValueError: - strport = '' -except TypeError: - strport = '' -strport = enter_integer(help, "port number", default=strport, twice=True) -configsubdict['fssd:port'] = strport - - -help = """ - -Absolute path to the template used to generate the subject of the email message -sent by the file-safe shutdown daemon to indicate power failure as detected -by the UPS PIco - -""" - -prompt =" absolute path to the email subject template" -subject_template = configsubdict['fssd:subject-template'] -subject_template = enter_string(help, prompt, default=subject_template, twice=False) -configsubdict['fssd:subject-template'] = subject_template - -help = """ - -Absolute path to the template used to generate the body of the email message -sent by the file-safe shutdown daemon to indicate power failure as detected -by the UPS PIco - -""" - -prompt =" absolute path to the email body template" -body_template = configsubdict['fssd:body-template'] -body_template = enter_string(help, prompt, default=body_template, twice=False) -configsubdict['fssd:body-template'] = body_template - - - - - -configuration.write_config_xml(args.output_xml, configdict) - - - +#!/usr/bin/python3 + +import argparse +import xmltodict +import getpass +import base64 +from pimodules import configuration + + +def get_yesno(help, prompt): + """ + Accepts either 'y' or 'n' at the command-line + """ + print(help) + yn = None + while yn not in ['y','n']: + yn = input(prompt) or 'x' + if yn not in ['y','n']: + print("Please answer 'y' or 'n'") + + return yn == 'y' + +def is_correct(prompt, strval): + """ + Asks the user whether a value entered is correct + """ + yn = None + while yn not in ['y','n']: + yn = input(format("You entered %s for the %s, is this correct? " % (strval, prompt))).lower() + if yn not in ['y','n']: + print("Please answer 'y' or 'n'") + + return yn == 'y' + +def enter_string(help, prompt, default=None, twice=True): + """ + Accepts a string value at the command line, asks the user to confirm whether it is the correct + value and optionally does the accept twice and compares the two occurrences + """ + print(help) + default = default or '' + savedefault = default + while True: + if default: + pt = format("Enter the value for the %s [ %s ]: " % (prompt, default)) + str1 = input(pt) or default + else: + pt = format("Enter the value for the %s: " % prompt) + str1 = input(pt) + if not str1: + print("You cannot enter a zero-length string") + continue + + if is_correct(prompt, str1): + if not twice: + return str1 + else: + if str1 != default: + default = '' + + if default: + pt = format("Re-enter the value for the %s (%s): " % (prompt, default)) + str2 = input(pt) or default + else: + pt = format("Re-enter the value for the %s: " % prompt) + str2 = input(pt) + + if str1 == str2: + break + else: + print("Values do not match") + + return str2 + + + +def enter_integer(help, prompt, default=None, twice=True): + """ + Accepts a string at the command-line. Checks whether the string is a valid + integer, optionally accepting it twice and comparing the two strings to make sure + they are the same + """ + print(help) + default = default or '' + savedefault = default + while True: + if default: + pt = format("Enter the value for the %s [ %s ]: " % (prompt, default)) + str1 = input(pt) or default + else: + pt = format("Enter the value for the %s: " % prompt) + str1 = input(pt) + if not str1: + print("You cannot enter a zero-length string") + continue + try: + integer = int(str1) + except ValueError: + print("You must enter a valid integer") + continue + except TypeError: + print("You must enter a valid integer") + continue + + if is_correct(prompt, str1): + if not twice: + return str1 + else: + if str1 != default: + default = '' + + if default: + pt = format("Re-enter the value for the %s (%s): " % (prompt, default)) + str2 = input(pt) or default + else: + pt = format("Re-enter the value for the %s: " % prompt) + str2 = input(pt) + + if str1 == str2: + break + else: + print("Values do not match") + + return str2 + + +def double_enter_password(help, prompt): + """ + Accepts a password entered at the command-line and checks both instances + are the same + """ + print(help) + while True: + pt = format("Enter the value for the %s: " % prompt) + str1 = getpass.getpass(pt) + pt = format("Re-enter the value for the %s: " % prompt) + str2 = getpass.getpass(pt) + + if str1 == str2: + break + else: + print("Values do not match") + + return str2 + + +def get_choice(help, prompt, choices, show=False): + """ + Accepts a string at the command-line. Choice entered must be one of the choices in the list. Optionally shows the choices + available in the prompt + """ + print(help) + choice = '' + if show: + prompt += '(' + ','.join(choices) + ')' + + while choice.lower() not in choices: + try: + choice = input(prompt).lower() + except EOFError: + raise KeyboardInterrupt + + return choice + + +#-- main program + +parser = argparse.ArgumentParser() + +group = parser.add_mutually_exclusive_group(required=True) +group.add_argument("-d", "--default", + help="Start from a fresh XML configuration, do not initialize values from existing XML file", + action="store_true", default=False) +group.add_argument("-i", "--input-xml", + help="Read initial configuration values from an existing file") +parser.add_argument("-o", "--output-xml", + help="Output XML configuration file", + default='picofssd.xml', required=True) + +args = parser.parse_args() + +if args.default: + configdict = configuration.read_config_xml(configuration.DEFAULT_FSSD_XML_CONFIG, True) +else: + configdict = configuration.read_config_xml(args.input_xml, False) + + +configsubdict = configdict['root']['fssd:config']['fssd:alerts']['fssd:email'] + +help = """ + +Follow the prompts and enter strings for the XML configuration which drives +the UPS PIco file-safe shutdown daemon. + +Default values read from the previous XML configuration are shown in square brackets. Just press enter +to accept a default value. + +""" + +enabled = get_yesno(help, "Do you want to enable the email alert? ('y', 'n'): ") +configsubdict['fssd:enabled'] = enabled + + +help = """ + +Email server. This is usually something like 'mail.domain.com', where 'domain' reflects the company providing the +email account. + +""" + +server = configsubdict['fssd:server'] +server = enter_string(help, "mail server", default=server,twice=True) +configsubdict['fssd:server'] = server + +help = """ + +Email account user name. This is the user name used to log into the email server. This is usually +the full email address of the account holder. + +""" + +username = configsubdict['fssd:username'] +username = enter_string(help, "email user name (this is usually the full email address)", default=username, twice=True) +configsubdict['fssd:username'] = username + + +help = """ + +Email address of the originator (sender) of the email. This is usually the same as the user name if +the full email address is used as the server's log in string given above. + +""" + +sender = configsubdict['fssd:sender-email-address'] +sender = enter_string(help, "Sender's email address", default=sender, twice=True) +configsubdict['fssd:sender-email-address'] = sender + +help = """ + +The password for the email server and the email account. + +""" + +password = double_enter_password(help, "password") +configsubdict['fssd:sender-password'] = base64.b64encode(password.encode("utf-8")) + + +help = """ + +The recipient's email address to which the daemon should send an email alert. + +""" + +recipient = configsubdict['fssd:recipient-email-address'] +recipient = enter_string(help, "destination email address", default=recipient, twice=True) +configsubdict['fssd:recipient-email-address'] = recipient + + +help = """ + +The security method employed by the email SMTP server. Permitted methods are SSL (secure server layer) +or TLS (transport level security). + +""" + +security = get_choice(help, "Enter server security method ", ['ssl', 'tls'], True) +configsubdict['fssd:security'] = security + +help = """ + +The port number the email server uses. If security was 'SSL', this is probably 465. + +The value entered must be a valid integer. + +""" + +strport = configsubdict['fssd:port'] +try: + port = int(strport) +except ValueError: + strport = '' +except TypeError: + strport = '' +strport = enter_integer(help, "port number", default=strport, twice=True) +configsubdict['fssd:port'] = strport + + +help = """ + +Absolute path to the template used to generate the subject of the email message +sent by the file-safe shutdown daemon to indicate power failure as detected +by the UPS PIco + +""" + +prompt =" absolute path to the email subject template" +subject_template = configsubdict['fssd:subject-template'] +subject_template = enter_string(help, prompt, default=subject_template, twice=False) +configsubdict['fssd:subject-template'] = subject_template + +help = """ + +Absolute path to the template used to generate the body of the email message +sent by the file-safe shutdown daemon to indicate power failure as detected +by the UPS PIco + +""" + +prompt =" absolute path to the email body template" +body_template = configsubdict['fssd:body-template'] +body_template = enter_string(help, prompt, default=body_template, twice=False) +configsubdict['fssd:body-template'] = body_template + + + + + +configuration.write_config_xml(args.output_xml, configdict) diff --git a/code/python/upspico/picofssd/setup.cfg b/code/python/upspico/picofssd/setup.cfg deleted file mode 100644 index 01a5109..0000000 --- a/code/python/upspico/picofssd/setup.cfg +++ /dev/null @@ -1,3 +0,0 @@ -[install] -prefix=/usr/local - diff --git a/code/python/upspico/picofssd/setup.py b/code/python/upspico/picofssd/setup.py index 48affce..b834f78 100755 --- a/code/python/upspico/picofssd/setup.py +++ b/code/python/upspico/picofssd/setup.py @@ -1,43 +1,42 @@ - -"""picofssd: UPSPIco file-safe shutdown daemon - -This script is intended for use with the PiModules(R) UPSPIco -uninterruptible power supply for use with a Raspberry Pi computer. -""" - -classifiers = """\ -Development Status :: 5 - Testing/Beta -Intended Audience :: PiModules UPS PiCo Product developers and partners (change to customers when published) -License :: GNU General Public License Version 3 -Programming Language :: Python >= 2.7 -Topic :: PiModules(R) -Topic :: UPS PIco support daemon -Operating System :: Linux (Raspbian) -""" - - -from distutils.core import setup - -doclines = __doc__.split("\n") - - - -datafiles=[ - ('/etc/pimodules/picofssd', ['etc/pimodules/picofssd/emailAlertBody.template', 'etc/pimodules/picofssd/emailAlertSubject.template', 'etc/pimodules/picofssd/picofssd.xml']), - ('/etc/default', ['default/picofssd']), - ('/etc/init.d', ['init.d/picofssd']) -] - -setup(name='picofssd', - version='0.1dev', - description=doclines[0], - long_description = "\n".join(doclines[2:]), - license='GPL3', - author='Mike Ray', - author_email='mike.ray@btinternet.com', - url='http://pimodules.com', - platforms=['POSIX'], - classifiers = filter(None, classifiers.split("\n")), - scripts=['scripts/picofssd', 'scripts/picofssdxmlconfig'], - data_files = datafiles - ) + +"""picofssd: UPSPIco file-safe shutdown daemon + +This script is intended for use with the PiModules(R) UPSPIco +uninterruptible power supply for use with a Raspberry Pi computer. +""" + +from setuptools import setup + + +# classifiers = """\ +# Development Status :: 5 - Testing/Beta +# Intended Audience :: PiModules UPS PiCo Product developers and partners (change to customers when published) +# License :: GNU General Public License Version 3 +# Programming Language :: Python >= 2.7 +# Topic :: PiModules(R) +# Topic :: UPS PIco support daemon +# Operating System :: Linux (Raspbian) +# """ + + +doclines = __doc__.split("\n") +setup() +# moved to toml +# datafiles = [ +# ('.', ['scripts/picofssd.py', 'scripts/picofssdxmlconfig']) +# ] + +# setup(name='picofssd', +# version='0.2.dev1', +# description=doclines[0], +# long_description="\n".join(doclines[2:]), +# license='GPL3', +# author='Mike Ray', +# author_email='mike.ray@btinternet.com', +# url='http://pimodules.com', +# platforms=['POSIX'], +# classifiers=list(filter(None, classifiers.split("\n"))), +# install_requires=['fake-rpi', 'xmltodict', 'jinja2', 'rpi-lgpio'], +# packages=['scripts'], +# data_files=datafiles +# ) diff --git a/code/python/upspico/picofssd/systemd/picofssd.service b/code/python/upspico/picofssd/systemd/picofssd.service new file mode 100755 index 0000000..e4da48d --- /dev/null +++ b/code/python/upspico/picofssd/systemd/picofssd.service @@ -0,0 +1,16 @@ +[Unit] +Description=UPS PIco file-safe shutdown daemon + +[Service] +User=root +Restart=always +RestartSec=1s +StandardOutput=null +StandardError=null +#Deamon mode seems not to work +#ExecStart=/usr/bin/python3 /usr/local/bin/picofssd --log-level info --pid-file /var/run/picofssd.pid --xml-config /etc/pimodules/picofssd/picofssd.xml +ExecStart=/usr/bin/python3 -m picofssd --log-level info -d --xml-config /etc/pimodules/picofssd/picofssd.xml +TimeoutSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/code/shell/config_for_pico.sh b/code/shell/config_for_pico.sh index 0b17fb2..ab079e1 100755 --- a/code/shell/config_for_pico.sh +++ b/code/shell/config_for_pico.sh @@ -10,32 +10,66 @@ set -e echo '--- update' apt-get update echo '--- install some packages' -apt-get install -y python-dev python-pip python-serial python-smbus python-jinja2 wiringpi +apt-get install -y python3-dev python3-pip python3-serial python3-smbus python3-jinja2 python3-rpi.gpio python3-psutil python3-xmltodict # wiringpi -echo '--- pip install psutil' -pip install psutil +# echo '--- pip install psutil' +# pip3 install psutil -echo '--- pip install xmltodict' -pip install xmltodict +# echo '--- pip install xmltodict' +# pip3 install xmltodict echo '--- save and edit cmdline.txt' cp /boot/cmdline.txt /boot/cmdline.txt.save sed -i 's| console=serial0,115200 console=tty1||' /boot/cmdline.txt echo '--- save and edit config.txt' -cp /boot/config.txt /boot/config.txt.save -sed -i 's|#dtparam=i2c_arm=on|dtparam=i2c_arm=on|' /boot/config.txt +config="/boot/config.txt" +cp $config /boot/config.txt.save +echo '--- enable i2c' +if grep -R "dtparam=i2c_arm=on" $config +then + sed -i 's|#dtparam=i2c_arm=on|dtparam=i2c_arm=on|' $config +else + echo 'dtparam=i2c_arm=on' $config +fi + +echo '--- enable uart' +raspiuart=`cat $config | grep -R "enable_uart"` +if [ "$raspiuart" == "#enable_uart=1" ]; then + sed -i "s,$raspiuart,enable_uart=1," $config +elif [ "$raspiuart" == "#enable_uart=0" ]; then + sed -i "s,$raspiuart,enable_uart=1," $config +elif [ "$raspiuart" == "enable_uart=0" ]; then + sed -i "s,$raspiuart,enable_uart=1," $config +else + sh -c "echo 'enable_uart=1' >> $config" +fi -echo '--- adding line to config.txt' -echo -e "\n\ndtparam=pi3-disable-bt\n\n" >> /boot/config.txt +echo '--- enable rtc' +### Checking if rtc dtoverlay module is loaded which doesn't work on older kernels +rtcmodule=`cat $config | grep -R "dtoverlay=i2c-rtc,ds1307"` +if [ "$rtcmodule" == "#dtoverlay=i2c-rtc,ds1307" ]; then + sed -i -e 's/#dtoverlay=i2c-rtc,ds1307/dtoverlay=i2c-rtc,ds1307/g' $config +else + sh -c "echo 'dtoverlay=i2c-rtc,ds1307' >> $config" +fi +sleep 1 -echo '--- adding lines to /etc/modules' -echo -e "\n\ni2c-bcm2708\ni2c-dev\n\n" >> /etc/modules echo '--- disabling hciuart' systemctl disable hciuart +echo '--- disable hwclock enable ds1307' +apt-get -y remove fake-hwclock +update-rc.d -f fake-hwclock remove +systemctl disable fake-hwclock + +sed -i ':a;N;$!ba;s/if \[ -e \/run\/systemd\/system \] ; then\n exit 0\nfi/#if \[ -e \/run\/systemd\/system \] ; then\n# exit 0\n#fi/g' /lib/udev/hwclock-set +sed -i -e 's/ \/sbin\/hwclock --rtc=$dev --systz --badyear/ #\/sbin\/hwclock --rtc=$dev --systz --badyear/g' /lib/udev/hwclock-set +sed -i -e 's/ \/sbin\/hwclock --rtc=$dev --systz/ #\/sbin\/hwclock --rtc=$dev --systz/g' /lib/udev/hwclock-set +#do later in fabfile +#hwclock -w + echo '--- all done' exit 0 - diff --git a/pico_status/pico_status_hv3.0.py b/pico_status/pico_status_hv3.0.py index 9e7be5d..87387a3 100644 --- a/pico_status/pico_status_hv3.0.py +++ b/pico_status/pico_status_hv3.0.py @@ -1,10 +1,25 @@ #!/usr/bin/python ##################################################################################### -# pico_status_hv3.0.py -# updated: 20-01-2017 +# pico_status.py +# author : Kyriakos Naziris +# updated: 1-12-2017 # Script to show you some statistics pulled from your UPS PIco HV3.0A +# -*- coding: utf-8 -*- +# improved and completed by PiModules Version 1.0 29.08.2015 +# picoStatus-v3.py by KTB is based on upisStatus.py by Kyriakos Naziris +# Kyriakos Naziris / University of Portsmouth / kyriakos@naziris.co.uk +# +# As per 09-01-2017 +# Improved and modified for PiModules PIco HV3.0A Stack Plus / Plus / Top +# by Siewert Lameijer aka Siewert308SW +# added Watchdog timer functions by asr +##################################################################################### + +# You can install psutil using: sudo pip install psutil +# import psutil + ##################################################################################### # SETTINGS ##################################################################################### @@ -14,205 +29,356 @@ # F = Fahrenheit degrees = "C" +# Do you have a PIco FAN kit installed? +# True or False +fankit = False + +# Do you have a to92 temp sensor installed? +# True or False +to92 = False + +# Do you have extended power? +# True or False +extpwr = False + ##################################################################################### -# It's not necessary to edit anything below this line unless your knowing what to do! +# It's not necessary to edit anything below, unless you're knowing what to do! ##################################################################################### +# pip install filelock + +import os import smbus import time import datetime -i2c = smbus.SMBus(1) + +from filelock import Timeout, FileLock + +i2c = smbus.SMBus(1) # 0 = /dev/i2c-0 (port I2C0), 1 = /dev/i2c-1 (port I2C1) +lock = FileLock('/tmp/i2c.lock', timeout=-1) # evtl SoftFileLock verwenden + def fw_version(): - time.sleep(0.1) - data = i2c.read_byte_data(0x69, 0x26) - data = format(data,"02x") - return data + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x69, 0x26) + data = format(data, "02x") + return data + def boot_version(): - time.sleep(0.1) - data = i2c.read_byte_data(0x69, 0x25) - data = format(data,"02x") - return data + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x69, 0x25) + data = format(data, "02x") + return data + def pcb_version(): - time.sleep(0.1) - data = i2c.read_byte_data(0x69, 0x24) - data = format(data,"02x") - return data + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x69, 0x24) + data = format(data, "02x") + return data + def pwr_mode(): - data = i2c.read_byte_data(0x69, 0x00) - data = data & ~(1 << 7) - if (data == 1): - return "RPi POWERED" - elif (data == 2): - return "BAT POWERED" - else: - return "ERROR" + with lock: + data = i2c.read_byte_data(0x69, 0x00) + data = data & ~(1 << 7) + if (data == 1): + return "RPi POWERED" + elif (data == 2): + return "BAT POWERED" + else: + return "ERROR" + def bat_version(): - time.sleep(0.1) - data = i2c.read_byte_data(0x6b, 0x07) - if (data == 0x46): - return "LiFePO4 (ASCII : F)" - elif (data == 0x51): - return "LiFePO4 (ASCII : Q)" - elif (data == 0x53): - return "LiPO (ASCII: S)" - elif (data == 0x50): - return "LiPO (ASCII: P)" - else: - return "ERROR" + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x6b, 0x07) + if (data == 0x46): + return "LiFePO4 (ASCII : F)" + elif (data == 0x51): + return "LiFePO4 (ASCII : Q)" + elif (data == 0x53): + return "LiPO (ASCII: S)" + elif (data == 0x50): + return "LiPO (ASCII: P)" + else: + return "ERROR" + def bat_runtime(): - time.sleep(0.1) - data = i2c.read_byte_data(0x6b, 0x01) + 1 - if (data == 0x100): - return "TIMER DISABLED" - elif (data == 0xff): - return "TIMER DISABLED" - else: - data = str(data)+ " MIN" - return data + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x6b, 0x01) + 1 + if (data == 0x100): + return "TIMER DISABLED" + elif (data == 0xff): + return "TIMER DISABLED" + else: + data = str(data) + " MIN" + return data + def bat_level(): - time.sleep(0.1) - data = i2c.read_word_data(0x69, 0x08) - data = format(data,"02x") - return (float(data) / 100) + time.sleep(0.1) + with lock: + data = i2c.read_word_data(0x69, 0x08) + data = format(data, "02x") + return (float(data) / 100) + def bat_percentage(): - time.sleep(0.1) - datavolts = bat_level() - databattery = bat_version() - if (databattery == "LiFePO4 (ASCII : F)") or (databattery == "LiFePO4 (ASCII : Q)"): - datapercentage = ((datavolts-2.9)/1.25)*100 + time.sleep(0.1) + datavolts = bat_level() + databattery = bat_version() + if (databattery == "LiFePO4 (ASCII : F)") or (databattery == "LiFePO4 (ASCII : Q)"): + databatminus = datavolts-2.90 + datapercentage = ((databatminus/0.70))*100 + elif (databattery == "LiPO (ASCII: S)") or (databattery == "LiPO (ASCII: P)"): + databatminus = datavolts-3.4 + datapercentage = ((databatminus/0.899))*100 + return int(datapercentage) + + +def charger_state(): + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x69, 0x20) + battpercentage = bat_percentage() + powermode = pwr_mode() + databattery = bat_version() + if (databattery == "LiFePO4 (ASCII : F)") or (databattery == "LiFePO4 (ASCII : Q)"): + if (data == 0x00) and (powermode == "BAT POWERED"): + return "DISCHARGING" + if (data == 0x01) and (powermode == "RPi POWERED"): + return "CHARGING" + if (data == 0x00) and (powermode == "RPi POWERED"): + return "CHARGED" + if (databattery == "LiPO (ASCII: S)") or (databattery == "LiPO (ASCII: P)"): + if (data == 0x00) and (powermode == "BAT POWERED"): + return "DISCHARGING" + if (data == 0x00) and (powermode == "RPi POWERED"): + return "CHARGED" + if (data == 0x01) and (powermode == "RPi POWERED"): + return "CHARGING" - elif (databattery == "LiPO (ASCII: S)") or (databattery == "LiPO (ASCII: P)"): - datapercentage = ((datavolts-3.4)/0.75)*100 - return datapercentage def rpi_level(): - time.sleep(0.1) - data = i2c.read_word_data(0x69, 0x0a) - data = format(data,"02x") - return (float(data) / 100) + time.sleep(0.1) + with lock: + data = i2c.read_word_data(0x69, 0x0a) + data = format(data, "02x") + powermode = pwr_mode() + if (powermode == "RPi POWERED"): + return (float(data) / 100) + else: + return "0.0" + + +def rpi_cpu_temp(): + time.sleep(0.1) + with lock: + data = os.popen('vcgencmd measure_temp').readline() + data = (data.replace("temp=", "").replace("'C\n", "")) + if (degrees == "C"): + return data + elif (degrees == "F"): + return (float(data) * 9 / 5) + 32 + def ntc1_temp(): - time.sleep(0.1) - data = i2c.read_byte_data(0x69, 0x1b) - data = format(data,"02x") - if (degrees == "C"): - return data - elif (degrees == "F"): - return (float(data) * 9 / 5) + 32 + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x69, 0x1b) + data = format(data, "02x") + if (degrees == "C"): + return data + elif (degrees == "F"): + return (float(data) * 9 / 5) + 32 + def to92_temp(): - time.sleep(0.1) - data = i2c.read_byte_data(0x69, 0x1C) - data = format(data,"02x") - if (degrees == "C"): - return data - elif (degrees == "F"): - return (float(data) * 9 / 5) + 32 + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x69, 0x1C) + data = format(data, "02x") + if (degrees == "C"): + return data + elif (degrees == "F"): + return (float(data) * 9 / 5) + 32 + def epr_read(): - time.sleep(0.1) - data = i2c.read_word_data(0x69, 0x0c) - data = format(data,"02x") - return (float(data) / 100) + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x69, 0x0c) + data = format(data, "02x") + return (float(data) / 100) + def ad2_read(): - time.sleep(0.1) - data = i2c.read_word_data(0x69, 0x14) - data = format(data,"02x") - return (float(data) / 100) - + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x69, 0x07) + data = format(data, "02x") + return (float(data) / 100) + + def fan_mode(): - time.sleep(0.1) - data = i2c.read_byte_data(0x6b, 0x11) - data = data & ~(1 << 2) - if (data == 1): - return "ENABLED" - elif (data == 0): - return "DISABLED" - else: - return "ERROR" - -def fan_state(): - time.sleep(0.1) - data = i2c.read_byte_data(0x6b, 0x13) - data = data & ~(1 << 2) - if (data == 1): - return "ON" - elif (data == 0): - return "OFF" - else: - return "ERROR" + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x6b, 0x11) + data = data & ~(1 << 2) + if (data == 2): + return "AUTOMATIC" + elif (data == 1): + return "ON" + elif (data == 0): + return "OFF" + else: + return "ERROR" + def fan_speed(): - time.sleep(0.1) - data = i2c.read_word_data(0x6b, 0x12) - data = format(data,"02x") - return (float(data) * 10) - -def r232_state(): - time.sleep(0.1) - data = i2c.read_byte_data(0x6b, 0x02) - if (data == 0x00): - return "OFF" - elif (data == 0x01): - return "ON @ 4800 pbs" - elif (data == 0x02): - return "ON @ 9600 pbs" - elif (data == 0x03): - return "ON @ 19200 pbs" - elif (data == 0x04): - return "ON @ 34600 pbs" - elif (data == 0x05): - return "ON @ 57600 pbs" - elif (data == 0x0f): - return "ON @ 115200 pbs" - else: - return "ERROR" - -print " " -print "***********************************" -print " UPS PIco HV3.0A Status " -print "***********************************" -print " " -print " ","UPS PIco Firmware.....:",fw_version() -print " ","UPS PIco Bootloader...:",boot_version() -print " ","UPS PIco PCB Version..:",pcb_version() -print " ","UPS PIco BAT Version..:",bat_version() -print " ","UPS PIco BAT Runtime..:",bat_runtime() -print " ","UPS PIco r232 State...:",r232_state() -print " " -print " ","Powering Mode.........:",pwr_mode() -print " ","BAT Percentage........:",bat_percentage(),"%" -print " ","BAT Voltage...........:",bat_level(),"V" -print " ","RPi Voltage...........:",rpi_level(),"V" - -if (degrees == "C"): - print " ","NTC1 Temperature......:",ntc1_temp(),"C" - print " ","TO-92 Temperature.....:",to92_temp(),"C" -elif (degrees == "F"): - print " ","NTC1 Temperature......:",ntc1_temp(),"F" - print " ","TO-92 Temperature.....:",to92_temp(),"F" -else: - print " ","NTC1 Temperature......: please set your desired temperature symbol!" - print " ","TO-92 Temperature.....: please set your desired temperature symbol!" - -print " ","Extended Voltage......:",epr_read(),"V" -print " ","A/D2 Voltage..........:",ad2_read(),"V" -print " " -print " ","PIco FAN Mode.........:",fan_mode() -print " ","PIco FAN State........:",fan_state() -print " ","PIco FAN Speed........:",fan_speed(),"RPM" -print " " -print "***********************************" -print " Powered by PiCo " -print "***********************************" -print " " \ No newline at end of file + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x6b, 0x12) + data = format(data, "02x") + return int(float(data) * 100) + + +def fan_threshold(): + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x6B, 0x14) + data = format(data, "02x") + if (degrees == "C"): + return data + elif (degrees == "F"): + return (float(data) * 9 / 5) + 32 + + +def rs232_state(): + time.sleep(0.1) + with lock: + data = i2c.read_byte_data(0x6b, 0x02) + if (data == 0x00): + return "OFF" + elif (data == 0xff): + return "OFF" + elif (data == 0x01): + return "ON @ 4800 pbs" + elif (data == 0x02): + return "ON @ 9600 pbs" + elif (data == 0x03): + return "ON @ 19200 pbs" + elif (data == 0x04): + return "ON @ 34600 pbs" + elif (data == 0x05): + return "ON @ 57600 pbs" + elif (data == 0x0f): + return "ON @ 115200 pbs" + else: + return "ERROR" + + +def reset(): + start_watchdog_timer(0) + + +def start_watchdog_timer(seconds): + seconds = int(seconds) + if seconds > 0xFE: + seconds = 0xFE + with lock: + i2c.write_byte_data(0x6b, 0x05, seconds) + + +def stop_watchdog_timer(): + with lock: + i2c.write_byte_data(0x6b, 0x05, 0xFF) + + +def read_watchdog_timer(): + out = -1 + with lock: + out = i2c.read_byte_data(0x6b, 0x05) + return out + + +if __name__ == '__main__': + print(" ") + print("**********************************************") + print("* UPS PIco HV3.0A Status *") + print("* Version 6.0 *") + print("**********************************************") + print(" ") + print(" ", "- PIco Firmware..........:", fw_version()) + print(" ", "- PIco Bootloader........:", boot_version()) + print(" ", "- PIco PCB Version.......:", pcb_version()) + print(" ", "- PIco BAT Version.......:", bat_version()) + print(" ", "- PIco BAT Runtime.......:", bat_runtime()) + print(" ", "- PIco rs232 State.......:", rs232_state()) + print(" ") + print(" ", "- Powering Mode..........:", pwr_mode()) + print(" ", "- Charger State..........:", charger_state()) + print(" ", "- Battery Percentage.....:", bat_percentage(), "%") + print(" ", "- Battery Voltage........:", bat_level(), "V") + print(" ", "- RPi Voltage............:", rpi_level(), "V") + print(" ") + + if (degrees == "C"): + print(" ", "- RPi CPU Temperature....:", rpi_cpu_temp(), "C") + print(" ", "- NTC1 Temperature.......:", ntc1_temp(), "C") + elif (degrees == "F"): + print(" ", "- RPi CPU Temperature....:", rpi_cpu_temp(), "F") + print(" ", "- NTC1 Temperature.......:", ntc1_temp(), "F") + else: + print(" ", "- RPi CPU Temperature....: please set temperature symbol in the script!") + print(" ", "- NTC1 Temperature.......: please set temperature symbol in the script!") + + if (to92 == True): + if (degrees == "C"): + print(" ", "- TO-92 Temperature......:", to92_temp(), "C") + elif (degrees == "F"): + print(" ", "- TO-92 Temperature......:", to92_temp(), "F") + else: + print(" ", "- TO-92 Temperature......: please set temperature symbol in the script!") + + if (extpwr == True): + print(" ", "- Extended Voltage.......:", epr_read(), "V") + print(" ", "- A/D2 Voltage...........:", ad2_read(), "V") + + if (fankit == True): + print(" ") + if (fan_mode() == "AUTOMATIC"): + print(" ", "- PIco FAN Mode..........:", fan_mode()) + if (degrees == "C"): + print(" ", "- PIco FAN Temp Threshold:", fan_threshold(), "C") + elif (degrees == "F"): + print(" ", "- PIco FAN Temp Threshold:", fan_threshold(), "F") + else: + print(" ", "- PIco FAN Temp Threshold: please set temperature symbol in the script!") + else: + print(" ", "- PIco FAN Mode..........:", fan_mode()) + if (fan_mode() == "ON"): + print(" ", "- PIco FAN Speed.........:", fan_speed(), "RPM") + else: + print(" ", "- PIco FAN Speed.........: 0 RPM") + + if (degrees == "C"): + print(" ", "- PIco FAN Temp Threshold:", fan_threshold(), "C") + elif (degrees == "F"): + print(" ", "- PIco FAN Temp Threshold:", fan_threshold(), "F") + else: + print(" ", "- PIco FAN Temp Threshold: please set temperature symbol in the script!") + print(" ") + print("**********************************************") + print("* Powered by PiModules *") + print("**********************************************") + print(" ")