diff --git a/README.md b/README.md index 290cbbc..6fd9ff4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,16 @@ - +**Codacy - static code analysis:** + +|branch|quality| +|---|---| +|master-branch|[](https://www.codacy.com/app/Schrolli91/BOSWatch/dashboard?bid=3763821)| +|develop-branch|[](https://www.codacy.com/app/Schrolli91/BOSWatch/dashboard?bid=3763820)| + +**Achtung:** Die readme ist veraltet - Neue Informationen werden im Laufe der Zeit in das Github Wiki integriert! + +**Attention:** This readme is outdated - New informations will be integrated in the Github Wiki! + + + :satellite: Python Script to receive and decode German BOS Information with rtl_fm and multimon-NG :satellite: @@ -24,6 +36,8 @@ unless you are developer you can use the develop-Branch - may be unstable! - Logfiles for better troubleshooting - verbose/quiet mode for more/none information - Ready for use BOSWatch as daemon +- possibility to start plugins asynchron +- NMA Error Handler ##### Features for the future: - more plugins @@ -43,6 +57,7 @@ If you want to code your own Plugin, see `plugins/README.md`. |BosMon|send data to BosMon server|:white_check_mark:|:white_check_mark:|:white_check_mark:| |firEmergency|send data to firEmergency server|:x:|:white_check_mark:|:white_check_mark:| |jsonSocket|send data as jsonString to a socket server|:white_check_mark:|:white_check_mark:|:white_check_mark:| +|NMA|send data to Notify my Android|:white_check_mark:|:white_check_mark:|:white_check_mark:| - for more Information to the plugins see `config.ini` @@ -68,7 +83,7 @@ No filter for a combination typ/plugin = all data will pass Syntax: `INDIVIDUAL_NAME = TYP;DATAFIELD;PLUGIN;FREQUENZ;REGEX` (separator `;`) - `TYP` = the data typ (FMS|ZVEI|POC) -- `DATAFIELD` = the field of the data array (See interface.txt) +- `DATAFIELD` = the field of the data array (see readme.md in plugin folder) - `PLUGIN` = the name of the plugin to call with this filter (* for all) - `FREQUENZ` = the frequenz to use the filter (for more SDR sticks (* for all)) - `REGEX` = the RegEX @@ -80,7 +95,7 @@ only POCSAG to MySQL with the text "ALARM:" in the message `pocTest = POC;msg;MySQL;*;ALARM:` ##### Web frontend (obsolete) -New version in future - old data in folder `/www/` +old data in folder `/exampeAddOns/simpleWeb/` ~~Put the files in folder /wwww/ into your local webserver folder (f.e. /var/www/). Now you must edit the "config.php" with your userdata to your local database. @@ -145,4 +160,3 @@ Thanks to smith_fms and McBo from Funkmeldesystem.de - Forum for Inspiration and ### Code your own Plugin See `plugins/README.md` - diff --git a/www/gfx/logo.png b/boswatch.png similarity index 100% rename from www/gfx/logo.png rename to boswatch.png diff --git a/boswatch.py b/boswatch.py index 9e63beb..071028e 100755 --- a/boswatch.py +++ b/boswatch.py @@ -1,5 +1,5 @@ #!/usr/bin/python -# -*- coding: cp1252 -*- +# -*- coding: UTF-8 -*- # """ BOSWatch @@ -24,11 +24,12 @@ import os # for log mkdir import time # for time.sleep() import subprocess # for starting rtl_fm and multimon-ng -from includes import globals # Global variables +from includes import globalVars # Global variables from includes import MyTimedRotatingFileHandler # extension of TimedRotatingFileHandler -from includes import converter # converter functions from includes import signalHandler # TERM-Handler for use script as a daemon from includes import checkSubprocesses # check startup of the subprocesses +from includes.helper import configHandler +from includes.helper import freqConverter # @@ -42,10 +43,11 @@ try: epilog="More options you can find in the extern config.ini file in the folder /config") # parser.add_argument("-c", "--channel", help="BOS Channel you want to listen") parser.add_argument("-f", "--freq", help="Frequency you want to listen", required=True) - parser.add_argument("-d", "--device", help="Device you want to use (Check with rtl_test)", type=int, default=0) + parser.add_argument("-d", "--device", help="Device you want to use (Check with rtl_test)", required=True) parser.add_argument("-e", "--error", help="Frequency-Error of your device in PPM", type=int, default=0) parser.add_argument("-a", "--demod", help="Demodulation functions", choices=['FMS', 'ZVEI', 'POC512', 'POC1200', 'POC2400'], required=True, nargs="+") parser.add_argument("-s", "--squelch", help="Level of squelch", type=int, default=0) + parser.add_argument("-g", "--gain", help="Level of gain", type=int, default=100) parser.add_argument("-u", "--usevarlog", help="Use '/var/log/boswatch' for logfiles instead of subdir 'log' in BOSWatch directory", action="store_true") parser.add_argument("-v", "--verbose", help="Shows more information", action="store_true") parser.add_argument("-q", "--quiet", help="Shows no information. Only logfiles", action="store_true") @@ -68,26 +70,27 @@ try: # initialization: rtl_fm = None multimon_ng = None + nmaHandler = None try: # # Script-pathes # - globals.script_path = os.path.dirname(os.path.abspath(__file__)) + globalVars.script_path = os.path.dirname(os.path.abspath(__file__)) # # Set log_path # if args.usevarlog: - globals.log_path = "/var/log/BOSWatch/" + globalVars.log_path = "/var/log/BOSWatch/" else: - globals.log_path = globals.script_path+"/log/" + globalVars.log_path = globalVars.script_path+"/log/" # # If necessary create log-path # - if not os.path.exists(globals.log_path): - os.mkdir(globals.log_path) + if not os.path.exists(globalVars.log_path): + os.mkdir(globalVars.log_path) except: # we couldn't work without logging -> exit print "ERROR: cannot initialize paths" @@ -103,7 +106,7 @@ try: #formatter = logging.Formatter('%(asctime)s - %(module)-15s %(funcName)-15s [%(levelname)-8s] %(message)s', '%d.%m.%Y %H:%M:%S') formatter = logging.Formatter('%(asctime)s - %(module)-15s [%(levelname)-8s] %(message)s', '%d.%m.%Y %H:%M:%S') # create a file logger - fh = MyTimedRotatingFileHandler.MyTimedRotatingFileHandler(globals.log_path+"boswatch.log", "midnight", interval=1, backupCount=999) + fh = MyTimedRotatingFileHandler.MyTimedRotatingFileHandler(globalVars.log_path+"boswatch.log", "midnight", interval=1, backupCount=999) # Starts with log level >= Debug # will be changed with config.ini-param later fh.setLevel(logging.DEBUG) @@ -132,12 +135,15 @@ try: # Clear the logfiles # fh.doRollover() - rtl_log = open(globals.log_path+"rtl_fm.log", "w") - mon_log = open(globals.log_path+"multimon.log", "w") + rtl_log = open(globalVars.log_path+"rtl_fm.log", "w") + mon_log = open(globalVars.log_path+"multimon.log", "w") + rawMmOut = open(globalVars.log_path+"mm_raw.txt", "w") rtl_log.write("") mon_log.write("") + rawMmOut.write("") rtl_log.close() mon_log.close() + rawMmOut.close() logging.debug("BOSWatch has started") logging.debug("Logfiles cleared") @@ -145,22 +151,22 @@ try: # It's an error, but we could work without that stuff... logging.error("cannot clear Logfiles") logging.debug("cannot clear Logfiles", exc_info=True) - pass # # For debug display/log args # try: - logging.debug("SW Version: %s",globals.getVers("vers")) - logging.debug("Build Date: %s",globals.getVers("date")) + logging.debug("SW Version: %s",globalVars.versionNr) + logging.debug("Build Date: %s",globalVars.buildDate) logging.debug("BOSWatch given arguments") if args.test: logging.debug(" - Test-Mode!") - logging.debug(" - Frequency: %s", converter.freqToHz(args.freq)) + logging.debug(" - Frequency: %s", freqConverter.freqToHz(args.freq)) logging.debug(" - Device: %s", args.device) logging.debug(" - PPM Error: %s", args.error) logging.debug(" - Squelch: %s", args.squelch) + logging.debug(" - Gain: %s", args.gain) demodulation = "" if "FMS" in args.demod: @@ -201,42 +207,59 @@ try: # try: logging.debug("reading config file") - globals.config = ConfigParser.ConfigParser() - globals.config.read(globals.script_path+"/config/config.ini") + globalVars.config = ConfigParser.ConfigParser() + globalVars.config.read(globalVars.script_path+"/config/config.ini") # if given loglevel is debug: - if globals.config.getint("BOSWatch","loglevel") == 10: - logging.debug(" - BOSWatch:") - for key,val in globals.config.items("BOSWatch"): - logging.debug(" -- %s = %s", key, val) - logging.debug(" - FMS:") - for key,val in globals.config.items("FMS"): - logging.debug(" -- %s = %s", key, val) - logging.debug(" - ZVEI:") - for key,val in globals.config.items("ZVEI"): - logging.debug(" -- %s = %s", key, val) - logging.debug(" - POC:") - for key,val in globals.config.items("POC"): - logging.debug(" -- %s = %s", key, val) + if globalVars.config.getint("BOSWatch","loglevel") == 10: + configHandler.checkConfig("BOSWatch") + configHandler.checkConfig("FMS") + configHandler.checkConfig("ZVEI") + configHandler.checkConfig("POC") except: # we couldn't work without config -> exit logging.critical("cannot read config file") logging.debug("cannot read config file", exc_info=True) exit(1) - # initialization was fine, continue with main program... + + # + # Set the loglevel and backupCount of the file handler + # try: - # - # Set the loglevel and backupCount of the file handler - # - logging.debug("set loglevel of fileHandler to: %s",globals.config.getint("BOSWatch","loglevel")) - fh.setLevel(globals.config.getint("BOSWatch","loglevel")) - logging.debug("set backupCount of fileHandler to: %s", globals.config.getint("BOSWatch","backupCount")) - fh.setBackupCount(globals.config.getint("BOSWatch","backupCount")) + logging.debug("set loglevel of fileHandler to: %s",globalVars.config.getint("BOSWatch","loglevel")) + fh.setLevel(globalVars.config.getint("BOSWatch","loglevel")) + logging.debug("set backupCount of fileHandler to: %s", globalVars.config.getint("BOSWatch","backupCount")) + fh.setBackupCount(globalVars.config.getint("BOSWatch","backupCount")) except: # It's an error, but we could work without that stuff... logging.error("cannot set loglevel of fileHandler") logging.debug("cannot set loglevel of fileHandler", exc_info=True) - pass + + + # + # Add NMA logging handler + # + try: + if configHandler.checkConfig("NMAHandler"): + # is NMAHandler enabled? + if globalVars.config.getboolean("NMAHandler", "enableHandler") == True: + # we only could do something, if an APIKey is given: + if len(globalVars.config.get("NMAHandler","APIKey")) > 0: + logging.debug("add NMA logging handler") + from includes import NMAHandler + if globalVars.config.get("NMAHandler","appName") == "": + nmaHandler = NMAHandler.NMAHandler(globalVars.config.get("NMAHandler","APIKey")) + else: + nmaHandler = NMAHandler.NMAHandler(globalVars.config.get("NMAHandler","APIKey"), globalVars.config.get("NMAHandler","appName")) + nmaHandler.setLevel(globalVars.config.getint("NMAHandler","loglevel")) + myLogger.addHandler(nmaHandler) + except: + # It's an error, but we could work without that stuff... + logging.error("cannot add NMA logging handler") + logging.debug("cannot add NMA logging handler", exc_info=True) + + + # initialization was fine, continue with main program... # # Load plugins @@ -254,27 +277,25 @@ try: # Load filters # try: - if globals.config.getboolean("BOSWatch","useRegExFilter"): - from includes import filter - filter.loadFilters() + if globalVars.config.getboolean("BOSWatch","useRegExFilter"): + from includes import regexFilter + regexFilter.loadFilters() except: # It's an error, but we could work without that stuff... logging.error("cannot load filters") logging.debug("cannot load filters", exc_info=True) - pass # # Load description lists # try: - if globals.config.getboolean("FMS","idDescribed") or globals.config.getboolean("ZVEI","idDescribed") or globals.config.getboolean("POC","idDescribed"): + if globalVars.config.getboolean("FMS","idDescribed") or globalVars.config.getboolean("ZVEI","idDescribed") or globalVars.config.getboolean("POC","idDescribed"): from includes import descriptionList descriptionList.loadDescriptionLists() except: # It's an error, but we could work without that stuff... logging.error("cannot load description lists") logging.debug("cannot load description lists", exc_info=True) - pass # # Start rtl_fm @@ -283,13 +304,13 @@ try: if not args.test: logging.debug("starting rtl_fm") command = "" - if globals.config.has_option("BOSWatch","rtl_path"): - command = globals.config.get("BOSWatch","rtl_path") - command = command+"rtl_fm -d "+str(args.device)+" -f "+str(converter.freqToHz(args.freq))+" -M fm -s 22050 -p "+str(args.error)+" -E DC -F 0 -l "+str(args.squelch)+" -g 100" + if globalVars.config.has_option("BOSWatch","rtl_path"): + command = globalVars.config.get("BOSWatch","rtl_path") + command = command+"rtl_fm -d "+str(args.device)+" -f "+str(freqConverter.freqToHz(args.freq))+" -M fm -p "+str(args.error)+" -E DC -F 0 -l "+str(args.squelch)+" -g "+str(args.gain)+" -s 22050" rtl_fm = subprocess.Popen(command.split(), #stdin=rtl_fm.stdout, stdout=subprocess.PIPE, - stderr=open(globals.log_path+"rtl_fm.log","a"), + stderr=open(globalVars.log_path+"rtl_fm.log","a"), shell=False) # rtl_fm doesn't self-destruct, when an error occurs # wait a moment to give the subprocess a chance to write the logfile @@ -310,13 +331,13 @@ try: if not args.test: logging.debug("starting multimon-ng") command = "" - if globals.config.has_option("BOSWatch","multimon_path"): - command = globals.config.get("BOSWatch","multimon_path") + if globalVars.config.has_option("BOSWatch","multimon_path"): + command = globalVars.config.get("BOSWatch","multimon_path") command = command+"multimon-ng "+str(demodulation)+" -f alpha -t raw /dev/stdin - " multimon_ng = subprocess.Popen(command.split(), stdin=rtl_fm.stdout, stdout=subprocess.PIPE, - stderr=open(globals.log_path+"multimon.log","a"), + stderr=open(globalVars.log_path+"multimon.log","a"), shell=False) # multimon-ng doesn't self-destruct, when an error occurs # wait a moment to give the subprocess a chance to write the logfile @@ -338,16 +359,25 @@ try: while True: decoded = str(multimon_ng.stdout.readline()) #Get line data from multimon stdout from includes import decoder - decoder.decode(converter.freqToHz(args.freq), decoded) + decoder.decode(freqConverter.freqToHz(args.freq), decoded) + # write multimon-ng raw data + if globalVars.config.getboolean("BOSWatch","writeMultimonRaw"): + try: + rawMmOut = open(globalVars.log_path+"mm_raw.txt", "a") + rawMmOut.write(decoded) + except: + logging.warning("cannot write raw multimon data") + finally: + rawMmOut.close() else: logging.debug("start testing") - testFile = open(globals.script_path+"/testdata/testdata.txt","r") + testFile = open(globalVars.script_path+"/testdata/testdata.txt","r") for testData in testFile: if (len(testData.rstrip(' \t\n\r')) > 1) and ("#" not in testData[0]): logging.info("Testdata: %s", testData.rstrip(' \t\n\r')) from includes import decoder - decoder.decode(converter.freqToHz(args.freq), testData) + decoder.decode(freqConverter.freqToHz(args.freq), testData) time.sleep(1) logging.debug("test finished") @@ -381,7 +411,13 @@ finally: finally: # Close Logging logging.debug("close Logging") + # Waiting for all Threads to write there logs + if globalVars.config.getboolean("BOSWatch","processAlarmAsync") == True: + logging.debug("waiting 3s for threads...") + time.sleep(3) logging.info("BOSWatch exit()") logging.shutdown() + if nmaHandler: + nmaHandler.close() fh.close() ch.close() diff --git a/config/config.template.ini b/config/config.template.ini index 69efe4c..33a7de9 100644 --- a/config/config.template.ini +++ b/config/config.template.ini @@ -23,6 +23,12 @@ backupCount = 7 #rtl_path = /usr/local/bin/ #multimon_path = /usr/local/bin/ +# if you are using many Plugins or Plugins with a long execution time +# you could execute them in an asynchronous manner +# It must be pointed out that enabling (0|1) this consume time, +# so don't use it for one rapid Plugin +processAlarmAsync = 0 + # Using RegEx-Filter (0 - off | 1 - on) # Filter-configuration in section [Filters] useRegExFilter = 0 @@ -42,14 +48,35 @@ doubleFilter_ignore_time = 5 # you will get more then one alarm anyway if the msg is different (receiving-problems) doubleFilter_check_msg = 0 +# writes the multimon-ng raw data stream into a text file named mm_raw.txt +writeMultimonRaw = 1 + +[NMAHandler] +# you could use an logging handler for sending logging records to NotifyMyAndroid +# enableHandler (0|1) will enable the NMA handler +enableHandler = 0 + +# loglevel for NMAHandler (see BOSWatch loglevel description) +loglevel = 50 + +# logging record will send to APIKey +APIKey = + +# You could change the name of the application (default: BOSWatch) +# (f.e. if you use more than one instance of BOSWatch) +appName = BOSWatch + + [FMS] # look-up-table for adding a description # Using Description (0 - off | 1 - on) +# descriptions loaded from csv/fms.csv idDescribed = 0 [ZVEI] # look-up-table for adding a description # Using Description (0 - off | 1 - on) +# descriptions loaded from csv/zvei.csv idDescribed = 0 [POC] @@ -68,8 +95,14 @@ filter_range_end = 9999999 # look-up-table for adding a description # Using Description (0 - off | 1 - on) +# descriptions loaded from csv/poc.csv idDescribed = 0 +# Static Massages for Subrics. +rica = Feuer +ricb = TH +ricc = AGT +ricd = Unwetter [Filters] # RegEX Filter Configuration @@ -77,7 +110,7 @@ idDescribed = 0 # No Filter for a Typ/Plugin Combination = all Data pass # INDIVIDUAL_NAME = TYP;DATAFIELD;PLUGIN;FREQUENZ;REGEX # TYP = the Data Typ (FMS|ZVEI|POC) -# DATAFIELD = the field of the Data Array (See interface.txt) +# DATAFIELD = the field of the Data Array (see readme.md in plugin folder) # PLUGIN = the name of the Plugin to call with this Filter (* for all) # FREQUENZ = the Frequenz to use the Filter (for more SDR Sticks (* for all)) # REGEX = the RegEX @@ -97,6 +130,12 @@ eMail = 0 BosMon = 0 firEmergency = 0 jsonSocket = 0 +notifyMyAndroid = 0 +SMS = 0 +Sms77 = 0 +FFAgent = 0 +Pushover = 0 +Telegram = 0 # for developing template-module template = 0 @@ -105,7 +144,7 @@ template = 0 [MySQL] # MySQL configuration dbserver = localhost -dbuser = root +dbuser = boswatch dbpassword = root database = boswatch @@ -113,10 +152,15 @@ database = boswatch tableFMS = bos_fms tableZVEI = bos_zvei tablePOC = bos_pocsag +tableSIG = bos_signal + +# Signal RICs (empty: none set, separator ",") +# f.e.: signal_ric = 1234566,1234567,1234568 +signal_ric = ## SIGNAL-RICS ## [httpRequest] -# URL without http:// +# example URL http://example.com/remote.php?DESCR=%DESCR% # you can use the following wildcards in your URL as GET params: # http://en.wikipedia.org/wiki/Query_string @@ -129,25 +173,26 @@ tablePOC = bos_pocsag # %DESCR% = Description from csv-file # %TIME% = Time (by script) # %DATE% = Date (by script) -#fms_url = www.google.de?code=%FMS%&stat=%STATUS% +#fms_url = http://www.google.de?code=%FMS%&stat=%STATUS% fms_url = # %ZVEI% = ZVEI 5-tone Code # %DESCR% = Description from csv-file # %TIME% = Time (by script) # %DATE% = Date (by script) -#zvei_url = www.google.de?zvei=%ZVEI% +#zvei_url = http://www.google.de?zvei=%ZVEI% zvei_url = # %RIC% = Pocsag RIC # %FUNC% = Pocsac function/Subric (1-4) # %FUNCCHAR% = Pocsac function/Subric als character (a-d) +# %FUNCTEXT% = Pocsac function/Subric static massage definded in pocsag section # %MSG% = Message of the Pocsag telegram # %BITRATE% = Bitrate of the Pocsag telegram # %DESCR% = Description from csv-file # %TIME% = Time (by script) # %DATE% = Date (by script) -#poc_url = www.google.de?ric=%RIC%&subric=%FUNC%&msg=%MSG% +#poc_url = http://www.google.de?ric=%RIC%&subric=%FUNC%&msg=%MSG% poc_url = @@ -171,34 +216,44 @@ to = user@irgendwo, user2@woanders # normal|urgent|non-urgent priority = urgent -# %FMS% = FMS Code -# %STATUS% = FMS Status -# %DIR% = Direction of the telegram (0/1) -# %DIRT% = Direction of the telegram (Text-String) -# %TSI% = Tactical Short Information (I-IV) -# %DESCR% = Description from csv-file -# %TIME% = Time (by script) -# %DATE% = Date (by script) +# %FMS% = FMS Code +# %STATUS% = FMS Status +# %DIR% = Direction of the telegram (0/1) +# %DIRT% = Direction of the telegram (Text-String) +# %TSI% = Tactical Short Information (I-IV) +# %DESCR% = Description, if description-module is used +# %DATE% = Date (by script) +# %TIME% = Time (by script) +# %BR% = Insert line wrap (only in message) +# %LPAR% = ( +# %RPAR% = ) fms_subject = FMS: %FMS% -fms_message = %DATE% %TIME%: %FMS% - Status: %STATUS% - Direction: %DIRT% - TSI: %TSI% +fms_message = %DATE% %TIME%: %FMS%%BR%Status: %STATUS% - Direction: %DIRT% - TSI: %TSI% -# %ZVEI% = ZVEI 5-tone Code -# %DESCR% = Description from csv-file -# %TIME% = Time (by script) -# %DATE% = Date (by script) +# %ZVEI% = ZVEI 5-tone Code +# %DESCR% = Description, if description-module is used +# %DATE% = Date (by script) +# %TIME% = Time (by script) +# %BR% = Insert line wrap (only in message) +# %LPAR% = ( +# %RPAR% = ) zvei_subject = Alarm: %ZVEI% zvei_message = %DATE% %TIME%: %ZVEI% -# %RIC% = Pocsag RIC -# %FUNC% = Pocsac function/Subric (1-4) +# %RIC% = Pocsag RIC +# %FUNC% = Pocsac function/Subric (1-4) # %FUNCCHAR% = Pocsac function/Subric als character (a-d) -# %MSG% = Message of the Pocsag telegram -# %BITRATE% = Bitrate of the Pocsag telegram -# %DESCR% = Description from csv-file -# %TIME% = Time (by script) -# %DATE% = Date (by script) -poc_subject = Alarm: %RIC%%FUNCCHAR% -poc_message = %DATE% %TIME%: %MSG% +# %FUNCTEXT% = Pocsac function/Subric static massage definded in pocsag section +# %MSG% = Message of the Pocsag telegram +# %BITRATE% = Bitrate of the Pocsag telegram +# %DESCR% = Description, if description-module is used +# %DATE% = Date (by script) +# %TIME% = Time (by script) +# %BR% = Insert line wrap (only in message) +# %LPAR% = ( +# %RPAR% = ) +poc_subject = Alarm: %RIC%%LPAR%%FUNCCHAR%%RPAR% +poc_message = %DATE% %TIME% - %DESCR%: %MSG% [BosMon] @@ -220,6 +275,7 @@ bosmon_password = firserver = localhost firport = 9001 + [jsonSocket] # Protocol for socket (TCP|UDP) protocol = UDP @@ -228,6 +284,169 @@ server = 192.168.0.1 port = 8888 +[notifyMyAndroid] +# this APIKey is used for Plugin +APIKey = + +# Priority goes from -2 (lowest) to 2 (highest). the default priority is 0 (normal) +priority = 0 + +# You could change the name of the application (default: BOSWatch) +# (f.e. if you use more than one instance of BOSWatch) +appName = BOSWatch + +# instead of a given APIKey/priority you could import them by a csv-file (0|1) +# APIKey and prioiry above will be ignored, if you use a csv +# configuration loaded from csv/nma.csv +usecsv = 0 + + +[SMS] +# be aware that you need 'gammu' installed and running +# at least you need an UMTS-stick which is supported by 'gammu' + +quantity = 1 +# be sensitive to single RIC +ric1 = 1234567 + +# but you can watch several subrics, comma-separated +subric1 = a, b + +# a single cellphone-number +phonenumber1 = 0160321654987 + +# and the text for the sms +# ! DO NOT USE ANY UMLAUT ! +text1 = Rueckruf Leitstelle! + + +[Sms77] +# SMS77 configuration +# Login Username +user = + +# Password or API Key +password = + +# Receiver singlenumber or groupname from adressbook +to = + +# Sender number or name +from = + +# Type of Message (see https://www.sms77.de/funktionen/smstypen and https://www.sms77.de/funktionen/http-api) +type = quality + + +[FFAgent] +# set live mode (0/1) +live = 0 + +# send messages as type test (0/1) +test = 1 + +# path to server certificate file +serverCertFile = + +# path to client certificate file (LIVE) +clientCertFile = + +# path to client certificate password file (LIVE) +clientCertPass = + +# webapi token +webApiToken = + +# webapi key +webApiKey = + +# access token +accessToken = + +# selective Call Code +selectiveCallCode = + + +[Pushover] +# Pushover API Key +api_key = + +# Pushover Userkey or Groupkey to receive message +user_key = + +# Title of the message +title = BOSWatch Message + +# Adapt Pocsag Subric (a,b,c,d) to Pushover Priorities (see https://pushover.net/api#priority) +SubA = 0 +SubB = 2 +SubC = 1 +SubD = 0 + +# how often should Pushover re-alert in seconds (emergency-messages) +retry = 30 + +# when should Pushover stop to re-alert in seconds (emergency-messages) +expire = 90 + +# use HTML in messages (0/1) +html = 1 + + +[Telegram] +# This is your unique BOT token. You will get it from the BotFather once you have created your BOT. +BOTTokenAPIKey = +# Create a group chat with your BOT and enter the chat ID here. +# The plugin will send messages as your BOT and post everything in this group chat. +BOTChatIDAPIKey = +# The plugin can extract a location from the POCSAG message. +# However, this will be done for the following RIC only (7 digits e.g. 0012345). +RICforLocationAPIKey = +# This is your Google API key. +# Required if you want to create a map based on location information received with the above RIC. +GoogleAPIKey = + +[yowsup] +# number or chat-number who whants to become the news +empfaenger = +# WhatsApp-number of that the news comes +sender = +# password from this number +password= + +# %FMS% = FMS Code +# %STATUS% = FMS Status +# %DIR% = Direction of the telegram (0/1) +# %DIRT% = Direction of the telegram (Text-String) +# %TSI% = Tactical Short Information (I-IV) +# %DESCR% = Description, if description-module is used +# %DATE% = Date (by script) +# %TIME% = Time (by script) +# %LPAR% = ( +# %RPAR% = ) +fms_message = %DATE% %TIME%: %FMS% + +# %ZVEI% = ZVEI 5-tone Code +# %DESCR% = Description, if description-module is used +# %DATE% = Date (by script) +# %TIME% = Time (by script) +# %LPAR% = ( +# %RPAR% = ) +zvei_message = %DATE% %TIME%: %ZVEI% + +# %RIC% = Pocsag RIC +# %FUNC% = Pocsac function/Subric (1-4) +# %FUNCCHAR% = Pocsac function/Subric als character (a-d) +# %MSG% = Message of the Pocsag telegram +# %BITRATE% = Bitrate of the Pocsag telegram +# %DESCR% = Description, if description-module is used +# %DATE% = Date (by script) +# %TIME% = Time (by script) +# %LPAR% = ( +# %RPAR% = ) +poc_message = %MSG% + + ##################### ##### Not ready yet # ##################### diff --git a/csv/fms.csv b/csv/fms.csv index a9260fd..8ee6668 100644 --- a/csv/fms.csv +++ b/csv/fms.csv @@ -7,4 +7,4 @@ fms,description # # !!! DO NOT delete the first line !!! # -12345678,"FMS testdata" +12345678,"FMS testdata äöüß" diff --git a/csv/nma.csv b/csv/nma.csv new file mode 100644 index 0000000..f13795a --- /dev/null +++ b/csv/nma.csv @@ -0,0 +1,32 @@ +typ,id,APIKey,priority,eventPrefix,comment +# +# BOSWatch CSV file for notifyMyAndroid-Plugin +# +# For each id (FMS, ZVEI, POC) you could set multiple APIKeys with different prioties +# Use the structure: typ, id, APIKey, priority, eventPrefix, "comment" +# +# For more than one recipient you could you an id several times +# +# Typ: FMS|ZVEI|POC +# +# id: +# --- +# FMS: FMS Code +# ZVEI: ZVEI 5-tone Code +# POCSAG: POCSAG RIC + functionChar +# 1234567a = entry only for functionChar a +# 1234567* = entry for all functionChars +# +# Priority: goes from -2 (lowest) to 2 (highest). the default priority is 0 (normal) +# +# Event-Präfix: will be insert in front of "id/description" +# f.e.: "Alarm: 1234567" or "Alarm: POCSAG testdata äöüß" +# +# !!! DO NOT delete the first line !!! +# +POC,1000512*,123456789012345678901234567890123456789012345678,0,"","Test for *" +POC,1000000a,123456789012345678901234567890123456789012345678,-2,"Probe","Priority-Test" +POC,1000000a,234567890123456789012345678901234567890123456789,-1,"Probe","Priority-Test" +POC,1000001b,123456789012345678901234567890123456789012345678,2,"Alarm","Priority-Test" +POC,1000002c,123456789012345678901234567890123456789012345678,1,"Vor-Alarm","Priority-Test" +POC,1000003d,123456789012345678901234567890123456789012345678,0,"Info-Alarm","Priority-Test" diff --git a/csv/poc.csv b/csv/poc.csv index 44ddacb..87a5073 100644 --- a/csv/poc.csv +++ b/csv/poc.csv @@ -7,4 +7,4 @@ ric,description # # !!! DO NOT delete the first line !!! # -1234567,"POCSAG testdata" +1234567,"POCSAG testdata äöüß" diff --git a/csv/zvei.csv b/csv/zvei.csv index ebfc9e0..1be0548 100644 --- a/csv/zvei.csv +++ b/csv/zvei.csv @@ -7,4 +7,4 @@ zvei,description # # !!! DO NOT delete the first line !!! # -12345,"ZVEI testdata" +12345,"ZVEI testdata äöüß" diff --git a/exampleAddOns/alarmMonitorRPi/alarmMonitor.py b/exampleAddOns/alarmMonitorRPi/alarmMonitor.py new file mode 100644 index 0000000..f18bb15 --- /dev/null +++ b/exampleAddOns/alarmMonitorRPi/alarmMonitor.py @@ -0,0 +1,358 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# + +""" +alarmMonitor + +This is an alarmMonitor for receive alarm-messages from BOSWatch and show them on a touchscreen +The jsonSocketServer controlls an Watterott RPi-Display in case of received POCSAG-RIC + +Implemented functions: +- show ric-description and alarm-message on display +- history of up to 5 alarms +- different colours for no alarm, test alarm and alarm +- playing a soundfile in case of an alarm +- show POCSAG is alive status (coloured clock) +- asynchronous threads for display control +- auto-turn-off display +- status informations +- rotating logfile in /var/log/alarmMonitor + +@author: Jens Herrmann + +BOSWatch: https://github.com/Schrolli91/BOSWatch +RPi-Display: https://github.com/watterott/RPi-Display +""" + +import logging +import logging.handlers +import ConfigParser + +import os +import time +import socket # for socket +import json # for data +from threading import Thread +import pygame + +import globalData + +try: + # + # Logging + # + try: + # set/create log_path + log_path = "/var/log/alarmMonitor/" + if not os.path.exists(log_path): + os.mkdir(log_path) + + # init logger + myLogger = logging.getLogger() + myLogger.setLevel(logging.DEBUG) + formatter = logging.Formatter('%(asctime)s - %(module)-15s [%(levelname)-8s] %(message)s', '%d.%m.%Y %H:%M:%S') + + # display logger + ch = logging.StreamHandler() + ch.setLevel(logging.INFO) + #ch.setLevel(logging.DEBUG) + ch.setFormatter(formatter) + myLogger.addHandler(ch) + + # fileLogger: + fh = logging.handlers.TimedRotatingFileHandler(log_path+"alarmMonitor.log", "midnight", interval=1, backupCount=7) + fh.setLevel(logging.DEBUG) + fh.setFormatter(formatter) + myLogger.addHandler(fh) + # start with a new logfile + fh.doRollover() + except: + # we couldn't work without logging -> exit + logging.critical("cannot initialise logging") + logging.debug("cannot initialise logging", exc_info=True) + exit(1) + + + # + # Read config.ini + # + try: + logging.debug("reading config file") + globalData.config = ConfigParser.SafeConfigParser() + globalData.config.read("config.ini") + # if given loglevel is debug: + logging.debug("- [AlarmMonitor]") + for key,val in globalData.config.items("AlarmMonitor"): + logging.debug("-- %s = %s", key, val) + logging.debug("- [Display]") + for key,val in globalData.config.items("Display"): + logging.debug("-- %s = %s", key, val) + except: + # we couldn't work without config -> exit + logging.critical("cannot read config file") + logging.debug("cannot read config file", exc_info=True) + exit(1) + + # + # set environment for display and touchscreen + # + os.environ["SDL_FBDEV"] = "/dev/fb1" + os.environ["SDL_MOUSEDEV"] = "/dev/input/touchscreen" + os.environ["SDL_MOUSEDRV"] = "TSLIB" + + # + # start threads + # + try: + from displayServices import displayPainter, autoTurnOffDisplay, eventHandler + globalData.screenBackground = pygame.Color(globalData.config.get("AlarmMonitor","colourGreen")) + logging.debug("Start displayPainter-thread") + Thread(target=displayPainter).start() + logging.debug("start autoTurnOffDisplay-thread") + Thread(target=autoTurnOffDisplay).start() + logging.debug("start eventHandler-thread") + Thread(target=eventHandler).start() + except: + # we couldn't work without helper threads -> exit + logging.critical("cannot start service-Threads") + logging.debug("cannot start service-Threads", exc_info=True) + exit(1) + + # + # start socket + # + logging.debug("Start socketServer") + sock = socket.socket () # TCP + sock.bind(("",globalData.config.getint("AlarmMonitor","socketPort"))) + sock.listen(5) + logging.debug("socketServer runs") + + # + # Build Lists out of config-entries + # + logging.debug("create lists") + keepAliveRICs = [int(x.strip()) for x in globalData.config.get("AlarmMonitor","keepAliveRICs").replace(";", ",").split(",")] + logging.debug("-- keepAliveRICs: %s", keepAliveRICs) + alarmRICs = [int(x.strip()) for x in globalData.config.get("AlarmMonitor","alarmRICs").replace(";", ",").split(",")] + logging.debug("-- alarmRICs: %s", alarmRICs) + functionCharTestAlarm = [str(x.strip()) for x in globalData.config.get("AlarmMonitor","functionCharTestAlarm").replace(";", ",").split(",")] + logging.debug("-- functionCharTestAlarm: %s", functionCharTestAlarm) + functionCharAlarm = [str(x.strip()) for x in globalData.config.get("AlarmMonitor","functionCharAlarm").replace(";", ",").split(",")] + logging.debug("-- functionCharAlarm: %s", functionCharAlarm) + + # + # try to read History from MySQL-DB + # + try: + if globalData.config.getboolean("AlarmMonitor","loadHistory") == True: + import mysql.connector + + for key,val in globalData.config.items("MySQL"): + logging.debug("-- %s = %s", key, val) + + # Connect to DB + logging.debug("connect to MySQL") + connection = mysql.connector.connect(host = globalData.config.get("MySQL","dbserver"), user = globalData.config.get("MySQL","dbuser"), passwd = globalData.config.get("MySQL","dbpassword"), db = globalData.config.get("MySQL","database"), charset='utf8') + cursor = connection.cursor() + logging.debug("MySQL connected") + + # read countKeepAlive + # precondition: keepAliveRICs set + if (len(keepAliveRICs) > 0): + sql = "SELECT COUNT(*) FROM "+globalData.config.get("MySQL","tablePOC")+" WHERE ric IN ("+globalData.config.get("AlarmMonitor","keepAliveRICs")+")" + cursor.execute(sql) + result = int(cursor.fetchone()[0]) + if result > 0: + globalData.countKeepAlive = result + logging.debug("-- countKeepAlive: %s", globalData.countKeepAlive) + + # read countAlarm + # precondition: alarmRics and functionChar set + if (len(alarmRICs) > 0) and (len(functionCharAlarm) > 0): + sql = "SELECT COUNT(*) FROM "+globalData.config.get("MySQL","tablePOC")+" WHERE ric IN ("+globalData.config.get("AlarmMonitor","alarmRICs")+")" + if len(functionCharAlarm) == 1: + sql += " AND functionChar IN ('" + functionCharAlarm[0] + "')" + elif len(functionCharAlarm) > 1: + sql += " AND functionChar IN " + str(tuple(functionCharAlarm)) + cursor.execute(sql) + result = int(cursor.fetchone()[0]) + if result > 0: + globalData.countAlarm = result + logging.debug("-- countAlarm: %s", globalData.countAlarm) + + # read countTestAlarm + # precondition: alarmRics and functionCharTestAlarm set + if (len(alarmRICs) > 0) and (len(functionCharTestAlarm) > 0): + sql = "SELECT COUNT(*) FROM "+globalData.config.get("MySQL","tablePOC")+" WHERE ric IN ("+globalData.config.get("AlarmMonitor","alarmRICs")+")" + if len(functionCharTestAlarm) == 1: + sql += " AND functionChar IN ('" + functionCharTestAlarm[0] + "')" + elif len(functionCharTestAlarm) > 1: + sql += " AND functionChar IN " + str(tuple(functionCharTestAlarm)) + cursor.execute(sql) + result = int(cursor.fetchone()[0]) + if result > 0: + globalData.countTestAlarm = result + logging.debug("-- countTestAlarm: %s", globalData.countTestAlarm) + + # read the last 5 events in reverse order + # precondition: alarmRics and (functionChar or functionCharTestAlarm) set + if (len(alarmRICs) > 0) and ((len(functionCharAlarm) > 0) or (len(functionCharTestAlarm) > 0)): + sql = "SELECT UNIX_TIMESTAMP(time), ric, functionChar, msg, description FROM "+globalData.config.get("MySQL","tablePOC") + sql += " WHERE ric IN ("+globalData.config.get("AlarmMonitor","alarmRICs")+")" + functionChar = functionCharAlarm + functionCharTestAlarm + if len(functionChar) == 1: + sql += " AND functionChar IN ('" + functionChar[0] + "')" + elif len(functionChar) > 1: + sql += " AND functionChar IN " + str(tuple(functionChar)) + sql += " ORDER BY time DESC LIMIT 0,5" + cursor.execute(sql) + # reverse sort into history data + for (timestamp, ric, functionChar, msg, description) in reversed(cursor.fetchall()): + data = {} + data['timestamp'] = timestamp + data['ric'] = ric + data['functionChar'] = functionChar + data['msg'] = msg + data['description'] = description + globalData.alarmHistory.append(data) + logging.debug("-- history data loaded: %s", len(globalData.alarmHistory)) + + logging.info("history loaded from database") + # if db is enabled + pass + except: + # error, but we could work without history + logging.error("cannot load history from MySQL") + logging.debug("cannot load history from MySQL", exc_info=True) + pass + finally: + try: + cursor.close() + connection.close() + logging.debug("MySQL closed") + except: + pass + + # + # initialise alarm sound + # + alarmSound = False + try: + if globalData.config.getboolean("AlarmMonitor","playSound") == True: + if not globalData.config.get("AlarmMonitor","soundFile") == "": + pygame.mixer.init() + alarmSound = pygame.mixer.Sound(globalData.config.get("AlarmMonitor","soundFile")) + logging.info("alarm with sound") + except: + # error, but we could work without sound + logging.error("cannot initialise alarm sound") + logging.debug("cannot initialise alarm sound", exc_info=True) + pass + + globalData.startTime = int(time.time()) + logging.info("alarmMonitor started - on standby") + + # + # Main Program + # (Threads will set abort to True if an error occurs) + # + while globalData.abort == False: + # accept connections from outside + (clientsocket, address) = sock.accept() + logging.debug("connected client: %s", address) + + # recv message as json_string + json_string = clientsocket.recv( 4096 ) # buffer size is 1024 bytes + try: + # parsing jason + parsed_json = json.loads(json_string) + logging.debug("parsed message: %s", parsed_json) + except ValueError: + # we will ignore waste in json_string + logging.warning("No JSON object could be decoded: %s", json_string) + pass + else: + try: + logging.debug("Alarmmessage arrived") + logging.debug("-- ric: %s", parsed_json['ric']) + logging.debug("-- functionChar: %s", parsed_json['functionChar']) + + # current time for this loop: + curtime = int(time.time()) + + # keep alive calculation with additional RICs + if int(parsed_json['ric']) in keepAliveRICs: + logging.info("POCSAG is alive") + globalData.lastAlarm = curtime + globalData.countKeepAlive += 1 + + # (test) alarm processing + elif int(parsed_json['ric']) in alarmRICs: + if parsed_json['functionChar'] in functionCharTestAlarm: + logging.info("--> Probealarm: %s", parsed_json['ric']) + globalData.screenBackground = pygame.Color(globalData.config.get("AlarmMonitor","colourYellow")) + globalData.countTestAlarm += 1 + elif parsed_json['functionChar'] in functionCharAlarm: + logging.info("--> Alarm: %s", parsed_json['ric']) + globalData.screenBackground = pygame.Color(globalData.config.get("AlarmMonitor","colourRed")) + globalData.countAlarm += 1 + + # forward data to alarmMonitor + globalData.data = parsed_json + globalData.data['timestamp'] = curtime + logging.debug("-- data: %s", parsed_json) + # save 5 alarm history entries + globalData.alarmHistory.append(globalData.data) + if len(globalData.alarmHistory) > 5: + globalData.alarmHistory.pop(0) + # update lastAlarm for keep alive calculation + globalData.lastAlarm = curtime + # enable display for n seconds: + globalData.enableDisplayUntil = curtime + globalData.config.getint("AlarmMonitor","showAlarmTime") + # tell alarm-thread to turn on the display + globalData.navigation = "alarmPage" + globalData.showDisplay = True; + + # play alarmSound... + if not alarmSound == False: + # ... but only one per time... + if pygame.mixer.get_busy() == False: + alarmSound.play() + logging.debug("sound started") + + except KeyError: + # we will ignore waste in json_string + logging.warning("No RIC found: %s", json_string) + pass + +except KeyboardInterrupt: + logging.warning("Keyboard Interrupt") + exit(0) +except SystemExit: + logging.warning("SystemExit received") + exit(0) +except: + logging.exception("unknown error") +finally: + try: + logging.info("socketServer shuting down") + globalData.running = False + sock.close() + logging.debug("socket closed") + if not alarmSound == False: + pygame.mixer.quit() + logging.debug("mixer closed") + if connection: + connection.close() + logging.debug("MySQL closed") + time.sleep(0.5) + logging.debug("exiting socketServer") + except: + logging.warning("failed in clean-up routine") + finally: + # Close Logging + logging.debug("close Logging") + logging.info("socketServer exit()") + logging.shutdown() + ch.close() \ No newline at end of file diff --git a/exampleAddOns/alarmMonitorRPi/config.template.ini b/exampleAddOns/alarmMonitorRPi/config.template.ini new file mode 100644 index 0000000..82c7ba5 --- /dev/null +++ b/exampleAddOns/alarmMonitorRPi/config.template.ini @@ -0,0 +1,66 @@ +############################ +# AlarmMonitor Config File # +############################ + +[AlarmMonitor] +# listen port for socket server +socketPort = 8112 + +# process alarms for the following RICs +alarmRICs = 1234567, 12345678 + +# use the following RICs for keep alive calculation (additonal) +keepAliveRICs = 1000000 + +# use the following functionChar for test alarms (if empty no test alarms will shown) +functionCharTestAlarm = a + +# use the following functionChar for alarms (if empty no alarms will shown) +# if functionChar is used in functionCharTestAlarm too, it will bee ignored for alarms +functionCharAlarm = b, c, d + +# Show alarm massage for n seconds +showAlarmTime = 180 + +# Show display for n seconds by touching +showDisplayTime = 30 + +# colouring status of RIC-decoding (n seconds after last alarm) +delayForYellow = 240 +delayForRed = 360 + +# colours for alarmMonitor +colourBlack = #000000 +colourRed = #B22222 +colourGreen = #008B00 +colourBlue = #00008B +colourYellow = #8B8B00 +colourGrey = #BEBEBE +colourDimGrey = #696969 +colourWhite = #FFFFFF + +# alarm sound (0|1) and set filename +playSound = 0 +soundFile = sound.file + +# load history data from MySQL (0|1) +loadHistory = 0 + + +[Display] +# Pin of LCD backlight (script will use only on/off) +GPIOPinForBacklight = 18 + +# display size +displayWidth = 320 +displayHeight = 240 + + +[MySQL] +dbserver = localhost +dbuser = root +dbpassword = root +database = boswatch + +#tables in the database +tablePOC = bos_pocsag \ No newline at end of file diff --git a/exampleAddOns/alarmMonitorRPi/displayServices.py b/exampleAddOns/alarmMonitorRPi/displayServices.py new file mode 100644 index 0000000..8badbfe --- /dev/null +++ b/exampleAddOns/alarmMonitorRPi/displayServices.py @@ -0,0 +1,468 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# + +""" +alarmMonitor - displayServices + +@author: Jens Herrmann +""" + +# +# Only works as an asynchronous thread +# will call "exit(0)" when function is finished +# +def autoTurnOffDisplay(): + """ + Asynchronous function to turn of the display backlight + + @requires: globalData.showDisplay - status of backlight + @requires: globalData.enableDisplayUntil - given timestamp to turn off backlight + @requires: globalData.running - service runs as long as this is True + + In case of an exception the function set globalData.abort to True. + This will terminate the main program. + + @return: nothing + @exception: SystemExit exception in case of an error + """ + + import sys + import time + import logging + import ConfigParser + import globalData + + logging.debug("autoTurnOffDisplay-thread started") + + try: + # Running will be set to False if main program is shutting down + while globalData.running == True: + # check if timestamp is in the past + if (globalData.showDisplay == True) and (globalData.enableDisplayUntil < int(time.time())): + globalData.showDisplay = False + globalData.navigation = "alarmPage" + logging.info("display turned off") + # we will do this only one time per second + time.sleep(1) + except: + logging.error("unknown error in autoTurnOffDisplay-thread") + logging.debug("unknown error in autoTurnOffDisplay-thread", exc_info=True) + # abort main program + globalData.abort = True + sys.exit(1) + finally: + logging.debug("exit autoTurnOffDisplay-thread") + exit(0) + + +# +# Only works as an asynchronous thread +# will call "exit(0)" when function is finished +# +def eventHandler(): + """ + Asynchronous function to handle pygames events + in particular the touchscreen events + + @requires: globalData.showDisplay - status of backlight + @requires: globalData.enableDisplayUntil - timestamp to turn off backlight + @requires: globalData.running - service runs as long as this is True + @requires: configuration has to be set in the config.ini + + In case of an exception the function set globalData.abort to True. + This will terminate the main program. + + @return: nothing + @exception: SystemExit exception in case of an error + """ + import sys + import time + import logging + import ConfigParser + import pygame + import globalData + + logging.debug("eventHandler-thread started") + + try: + clock = pygame.time.Clock() + + # Running will be set to False if main program is shutting down + while globalData.running == True: + # This limits the while loop to a max of 2 times per second. + # Leave this out and we will use all CPU we can. + clock.tick(2) + + # current time for this loop: + curtime = int(time.time()) + + for event in pygame.event.get(): + # event-handler for QUIT + if event.type == pygame.QUIT: + globalData.running = False + + # if touchscreen pressed + if event.type == pygame.MOUSEBUTTONDOWN: + (posX, posY) = (pygame.mouse.get_pos() [0], pygame.mouse.get_pos() [1]) + #logging.debug("position: (%s, %s)", posX, posY) + + # touching the screen will stop alarmSound in every case + pygame.mixer.stop() + + # touching the dark display will turn it on for n sec + if globalData.showDisplay == False: + logging.info("turn ON display") + globalData.enableDisplayUntil = curtime + globalData.config.getint("AlarmMonitor","showDisplayTime") + globalData.navigation == "alarmPage" + globalData.showDisplay = True + else: + # touching the enabled display will be content sensitive... + # if top 2/3: turn of display + yBoundary = globalData.config.getint("Display","displayHeight") - 80 + if 0 <= posY <= yBoundary: + logging.info("turn OFF display") + globalData.showDisplay = False + globalData.navigation = "alarmPage" + else: + # we are in the navigation area + globalData.enableDisplayUntil = curtime + globalData.config.getint("AlarmMonitor","showDisplayTime") + if 0 <= posX <= 110: + globalData.navigation = "historyPage" + elif 111 <= posX <= 210: + globalData.navigation = "statusPage" + else: + globalData.screenBackground = pygame.Color(globalData.config.get("AlarmMonitor","colourGreen")) + globalData.navigation = "alarmPage" + ## end if showDisplay + ## end if event MOUSEBUTTONDOWN + ## end for event + except: + logging.error("unknown error in eventHandler-thread") + logging.debug("unknown error in eventHandler-thread", exc_info=True) + # abort main program + globalData.abort = True + sys.exit(1) + finally: + logging.debug("exit eventHandler-thread") + exit(0) + + +# +# Only works as an asynchronous thread +# will call "exit(0)" when function is finished +# +def displayPainter(): + """ + Asynchronous function to build the display content + + @requires: globalData.showDisplay - status of backlight + @requires: globalData.enableDisplayUntil - given timestamp when backlight will turned off + @requires: globalData.running - service runs as long as this is True + @requires: globalData.data - data of the last alarm + @requires: globalData.lastAlarm - timestamp of the last processing (see alarmRICs and keepAliveRICs) + @requires: configuration has to be set in the config.ini + + In case of an exception the function set globalData.abort to True. + This will terminate the main program. + + @return: nothing + @exception: SystemExit exception in case of an error + """ + + import sys + import time + import logging + import ConfigParser + import RPi.GPIO as GPIO + import pygame + from wrapline import wrapline + from roundrects import round_rect + import globalData + + logging.debug("displayPainter-thread called") + + try: + # use GPIO pin numbering convention + GPIO.setmode(GPIO.BCM) + GPIO.setwarnings(False) + # set up GPIO pin for output + GPIO.setup(globalData.config.getint("Display","GPIOPinForBacklight"), GPIO.OUT) + + pygame.init() + + #screen size + size = (globalData.config.getint("Display","displayWidth"), globalData.config.getint("Display","displayHeight")) + screen = pygame.display.set_mode(size) + + # disable mouse cursor + pygame.mouse.set_visible(False) + + #define fonts + fontHeader = pygame.font.Font(None, 30) + fontHeader.set_bold(True) + fontHeader.set_underline(True) + + fontTime = pygame.font.Font(None, 15) + + fontRIC = pygame.font.Font(None, 30) + fontRIC.set_bold(True) + fontMsg = pygame.font.Font(None, 20) + fontHistory = pygame.font.Font(None, 15) + + fontStatus = pygame.font.Font(None, 20) + fontStatus.set_bold(True) + fontStatusContent = pygame.font.Font(None, 20) + + fontButton = pygame.font.Font(None, 20) + + clock = pygame.time.Clock() + + # Build Lists out of config-entries + functionCharTestAlarm = [x.strip() for x in globalData.config.get("AlarmMonitor","functionCharTestAlarm").replace(";", ",").split(",")] + + logging.debug("displayPainter-thread started") + + # Running will be set to False if main program is shutting down + while globalData.running == True: + # This limits the while loop to a max of 2 times per second. + # Leave this out and we will use all CPU we can. + clock.tick(2) + + # current time for this loop: + curtime = int(time.time()) + + if globalData.showDisplay == True: + # Enable LCD display + GPIO.output(globalData.config.getint("Display","GPIOPinForBacklight"), GPIO.HIGH) + # Clear the screen and set the screen background + screen.fill(globalData.screenBackground) + # paint black rect, so Background looks like a boarder + widthX = globalData.config.getint("Display","displayWidth") - 20 + widthY = globalData.config.getint("Display","displayHeight") - 20 + pygame.draw.rect(screen, pygame.Color(globalData.config.get("AlarmMonitor","colourBlack")), (10, 10, widthX, widthY)) + # header + header = fontHeader.render("Alarm-Monitor", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourRed"))) + (width, height) = fontHeader.size("Alarm-Monitor") + x = (int(globalData.config.getint("Display","displayWidth")) - width)/2 + screen.blit(header, (x, 20)) + + # show time of last alarm + if globalData.lastAlarm > 0: + try: + # format last alarm + lastAlarmString = time.strftime("%H:%M:%S", time.localtime(globalData.lastAlarm)) + # Color time: + # red: lastAlarm more than n (delayForRed) seconds past + if (int(globalData.lastAlarm) + globalData.config.getint("AlarmMonitor","delayForRed")) < curtime: + timeColour = pygame.Color(globalData.config.get("AlarmMonitor","colourRed")) + # yellow: lastAlarm more than n (delayForYellow) seconds past + elif (int(globalData.lastAlarm) + globalData.config.getint("AlarmMonitor","delayForYellow")) < curtime: + timeColour = pygame.Color(globalData.config.get("AlarmMonitor","colourYellow")) + # dgrey: normal + else: + timeColour = pygame.Color(globalData.config.get("AlarmMonitor","colourGreen")) + lastAlarm = fontTime.render(lastAlarmString, 1, timeColour) + (width, height) = fontTime.size(lastAlarmString) + x = globalData.config.getint("Display","displayWidth") - 20 - width + screen.blit(lastAlarm, (x, 20)) + except: + logging.debug("unknown error in lastAlarm", exc_info=True) + pass + ## end if globalData.lastAlarm > 0 + + # show remaining time before display will be turned off: + restZeit = globalData.enableDisplayUntil - curtime +1 + zeit = fontTime.render(str(restZeit), 1, pygame.Color(globalData.config.get("AlarmMonitor","colourDimGrey"))) + screen.blit(zeit, (20, 20)) + + # + # content given by navigation: + # default is "alarmPage" + # + # Startpoint for content + if globalData.navigation == "historyPage": + try: + y = 50 + for data in reversed(globalData.alarmHistory): + # Layout: + # Date Description + # Time Msg + + # Prepare date/time block + dateString = time.strftime("%d.%m.%y", time.localtime(data['timestamp'])) + timeString = time.strftime("%H:%M:%S", time.localtime(data['timestamp'])) + if int(fontHistory.size(dateString)[0]) > int(fontHistory.size(timeString)[0]): + (shifting, height) = fontHistory.size(dateString) + else: + (shifting, height) = fontHistory.size(timeString) + shifting += 5 + + # get colour + if data['functionChar'] in functionCharTestAlarm: + colour = globalData.config.get("AlarmMonitor","colourYellow") + else: + colour = globalData.config.get("AlarmMonitor","colourRed") + + # Paint Date/Time + screen.blit(fontHistory.render(dateString, 1, pygame.Color(colour)), (20, y)) + screen.blit(fontHistory.render(timeString, 1, pygame.Color(colour)), (20, y + height)) + + # Paint Description + try: + textLines = wrapline(data['description'], fontHistory, (globalData.config.getint("Display","displayWidth") - shifting - 40)) + for index, item in enumerate(textLines): + textZeile = fontHistory.render(item, 1, pygame.Color(globalData.config.get("AlarmMonitor","colourWhite"))) + screen.blit(textZeile, (20 + shifting, y)) + y += height + except KeyError: + pass + + # Paint Msg + try: + textLines = wrapline(data['msg'].replace("*", " * "), fontHistory, (globalData.config.getint("Display","displayWidth") - shifting - 40)) + for index, item in enumerate(textLines): + textZeile = fontHistory.render(item, 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + screen.blit(textZeile, (20 + shifting, y)) + y += height + except KeyError: + pass + + # line spacing for next dataset + y += 2 + + ## end for globalData.alarmHistory + + except KeyError: + pass + ## end if globalData.navigation == "historyPage" + + elif globalData.navigation == "statusPage": + (width, height) = fontStatusContent.size("Anzahl Test-Alarme:") + y = 70 + x = width + 10 + # Running since: + title = fontStatusContent.render("Gestartet:", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourWhite"))) + content = fontStatusContent.render(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(globalData.startTime)), 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + screen.blit(title, (20, y)) + screen.blit(content, (20 +x, y)) + y += height + 10 + + # Last Alarm + title = fontStatusContent.render("Letzte Nachricht:", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourWhite"))) + if globalData.lastAlarm > 0: + content = fontStatusContent.render(time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(globalData.lastAlarm)), 1, timeColour) + else: + content = fontStatusContent.render("-", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + screen.blit(title, (20, y)) + screen.blit(content, (20 +x, y)) + y += height + 10 + + # Number of Alarms + title = fontStatusContent.render("Anzahl Alarme:", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourWhite"))) + content = fontStatusContent.render(str(globalData.countAlarm), 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + screen.blit(title, (20, y)) + screen.blit(content, (20 +x, y)) + y += height + 10 + + # Number of TestAlarms + title = fontStatusContent.render("Anzahl Test-Alarme:", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourWhite"))) + content = fontStatusContent.render(str(globalData.countTestAlarm), 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + screen.blit(title, (20, y)) + screen.blit(content, (20 +x, y)) + y += height + 10 + + # Number of DAU-Msgs + title = fontStatusContent.render("Anzahl DAU-Tests:", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourWhite"))) + content = fontStatusContent.render(str(globalData.countKeepAlive), 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + screen.blit(title, (20, y)) + screen.blit(content, (20 +x, y)) + y += height + 10 + + ## end if globalData.navigation == "statusPage" + + else: + y = 50 + + # Paint Date/Time + try: + dateTimeString = time.strftime("%d.%m.%Y %H:%M:%S", time.localtime(globalData.data['timestamp'])) + dateTimeRow = fontStatus.render(dateTimeString, 1, pygame.Color(globalData.config.get("AlarmMonitor","colourDimGrey"))) + (width, height) = fontStatus.size(dateTimeString) + x = (int(globalData.config.getint("Display","displayWidth")) - width)/2 + screen.blit(dateTimeRow, (x, y)) + y += height + 10 + except KeyError: + pass + + # Paint Description + try: + textLines = wrapline(globalData.data['description'], fontRIC, (globalData.config.getint("Display","displayWidth") - 40)) + (width, height) = fontStatus.size(globalData.data['description']) + for index, item in enumerate(textLines): + textRow = fontRIC.render(item, 1, pygame.Color(globalData.config.get("AlarmMonitor","colourWhite"))) + screen.blit(textRow, (20, y)) + y += height + 5 + except KeyError: + pass + + # Paint Msg + try: + y += 10 + textLines = wrapline(globalData.data['msg'].replace("*", " * "), fontMsg, (globalData.config.getint("Display","displayWidth") - 40)) + (width, height) = fontStatus.size(globalData.data['msg']) + for index, item in enumerate(textLines): + textRow = fontMsg.render(item, 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + screen.blit(textRow, (20, y)) + y += height + except KeyError: + pass + ## end if default navigation + + # paint navigation buttons + buttonWidth = 80 + buttonHeight = 25 + buttonY = globalData.config.getint("Display","displayHeight") - buttonHeight - 2 + + round_rect(screen, ( 20, buttonY, buttonWidth, buttonHeight), pygame.Color(globalData.config.get("AlarmMonitor","colourDimGrey")), 10, 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + buttonText = fontButton.render("Verlauf", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourBlack"))) + (width, height) = fontButton.size("Verlauf") + textX = 20 + (buttonWidth - width)/2 + textY = buttonY + (buttonHeight - height)/2 + screen.blit(buttonText, (textX, textY)) + + round_rect(screen, (120, buttonY, buttonWidth, buttonHeight), pygame.Color(globalData.config.get("AlarmMonitor","colourDimGrey")), 10, 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + buttonText = fontButton.render("Status", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourBlack"))) + (width, height) = fontButton.size("Status") + textX = 120 + (buttonWidth - width)/2 + textY = buttonY + (buttonHeight - height)/2 + screen.blit(buttonText, (textX, textY)) + + round_rect(screen, (220, buttonY, buttonWidth, buttonHeight), pygame.Color(globalData.config.get("AlarmMonitor","colourDimGrey")), 10, 1, pygame.Color(globalData.config.get("AlarmMonitor","colourGrey"))) + buttonText = fontButton.render("Gelesen", 1, pygame.Color(globalData.config.get("AlarmMonitor","colourBlack"))) + (width, height) = fontButton.size("Gelesen") + textX = 220 + (buttonWidth - width)/2 + textY = buttonY + (buttonHeight - height)/2 + screen.blit(buttonText, (textX, textY)) + + ## end if globalData.showDisplay == True + + else: + GPIO.output(globalData.config.getint("Display","GPIOPinForBacklight"), GPIO.LOW) + + # Update display... + pygame.display.update() + ## end while globalData.running == True + + except: + logging.error("unknown error in displayPainter-thread") + logging.debug("unknown error in displayPainter-thread", exc_info=True) + # abort main program + globalData.abort = True + sys.exit(1) + finally: + logging.debug("exit displayPainter-thread") + GPIO.output(globalData.config.getint("Display","GPIOPinForBacklight"), GPIO.LOW) + GPIO.cleanup() + pygame.quit() + exit(0) diff --git a/exampleAddOns/alarmMonitorRPi/globalData.py b/exampleAddOns/alarmMonitorRPi/globalData.py new file mode 100644 index 0000000..b80bbbd --- /dev/null +++ b/exampleAddOns/alarmMonitorRPi/globalData.py @@ -0,0 +1,35 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# + +# configparser (Configparser) +config = 0 + +# control-params (Boolean) +running = True +showDisplay = False +abort = False + +# color of display-boarder +screenBackground = "" + +# navigation = "" +navigation = "alarmPage" + +# enable display until (Timestamp) +enableDisplayUntil = 0 + +# data-structure (Dict) +data = {} + +# last alarm shown (Timestamp) +lastAlarm = 0 + +# alarm history (List) +alarmHistory = [] + +# statusPage +startTime = 0 +countAlarm = 0 +countTestAlarm = 0 +countKeepAlive = 0 \ No newline at end of file diff --git a/exampleAddOns/alarmMonitorRPi/roundrects.py b/exampleAddOns/alarmMonitorRPi/roundrects.py new file mode 100644 index 0000000..dbed0f8 --- /dev/null +++ b/exampleAddOns/alarmMonitorRPi/roundrects.py @@ -0,0 +1,63 @@ +""" +Rounded rectangles in both non-antialiased and antialiased varieties. + +@author: Sean J. McKiernan +https://github.com/Mekire/rounded-rects-pygame +""" + +import pygame as pg + +from pygame import gfxdraw + + +def round_rect(surface, rect, color, rad=20, border=0, inside=(0,0,0,0)): + """ + Draw a rect with rounded corners to surface. Argument rad can be specified + to adjust curvature of edges (given in pixels). An optional border + width can also be supplied; if not provided the rect will be filled. + Both the color and optional interior color (the inside argument) support + alpha. + """ + rect = pg.Rect(rect) + zeroed_rect = rect.copy() + zeroed_rect.topleft = 0,0 + image = pg.Surface(rect.size).convert_alpha() + image.fill((0,0,0,0)) + _render_region(image, zeroed_rect, color, rad) + if border: + zeroed_rect.inflate_ip(-2*border, -2*border) + _render_region(image, zeroed_rect, inside, rad) + surface.blit(image, rect) + + +def _render_region(image, rect, color, rad): + """Helper function for round_rect.""" + corners = rect.inflate(-2*rad, -2*rad) + for attribute in ("topleft", "topright", "bottomleft", "bottomright"): + pg.draw.circle(image, color, getattr(corners,attribute), rad) + image.fill(color, rect.inflate(-2*rad,0)) + image.fill(color, rect.inflate(0,-2*rad)) + + +def aa_round_rect(surface, rect, color, rad=20, border=0, inside=(0,0,0)): + """ + Draw an antialiased rounded rect on the target surface. Alpha is not + supported in this implementation but other than that usage is identical to + round_rect. + """ + rect = pg.Rect(rect) + _aa_render_region(surface, rect, color, rad) + if border: + rect.inflate_ip(-2*border, -2*border) + _aa_render_region(surface, rect, inside, rad) + + +def _aa_render_region(image, rect, color, rad): + """Helper function for aa_round_rect.""" + corners = rect.inflate(-2*rad-1, -2*rad-1) + for attribute in ("topleft", "topright", "bottomleft", "bottomright"): + x, y = getattr(corners, attribute) + gfxdraw.aacircle(image, x, y, rad, color) + gfxdraw.filled_circle(image, x, y, rad, color) + image.fill(color, rect.inflate(-2*rad,0)) + image.fill(color, rect.inflate(0,-2*rad)) diff --git a/exampleAddOns/alarmMonitorRPi/wrapline.py b/exampleAddOns/alarmMonitorRPi/wrapline.py new file mode 100644 index 0000000..1211ae4 --- /dev/null +++ b/exampleAddOns/alarmMonitorRPi/wrapline.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +# + +""" +alarmMonitor - wrapline + +This snippet of code will convert a string of text into a list containing the lines it would break down into for a certain font and width + +@author: pygame.org +http://www.pygame.org/wiki/TextWrapping +""" + +def truncline(text, font, maxwidth): + real=len(text) + stext=text + l=font.size(text)[0] + cut=0 + a=0 + done=1 + old = None + while l > maxwidth: + a=a+1 + n=text.rsplit(None, a)[0] + if stext == n: + cut += 1 + stext= n[:-cut] + else: + stext = n + l=font.size(stext)[0] + real=len(stext) + done=0 + return real, done, stext + +def wrapline(text, font, maxwidth): + done=0 + wrapped=[] + + while not done: + nl, done, stext=truncline(text, font, maxwidth) + wrapped.append(stext.strip()) + text=text[nl:] + return wrapped + + +def wrap_multi_line(text, font, maxwidth): + """ returns text taking new lines into account. + """ + lines = chain(*(wrapline(line, font, maxwidth) for line in text.splitlines())) + return list(lines) \ No newline at end of file diff --git a/www/index.php b/exampleAddOns/alarmMonitorWeb/Alarm.php similarity index 50% rename from www/index.php rename to exampleAddOns/alarmMonitorWeb/Alarm.php index aeaa785..deb1208 100644 --- a/www/index.php +++ b/exampleAddOns/alarmMonitorWeb/Alarm.php @@ -1,4 +1,21 @@ + + + + + + +einloggen"; + exit; + } +?> + +
-