#!/usr/bin/python3 -u
# The -u option above turns off block buffering of python output. This
# assures that each error message gets individually printed to the log file.
#
# Module: arednsigAgent.py
#
# Description: This module acts as an agent between the aredn node
# and aredn mest services. The agent periodically sends an http
# request to the aredn node, processes the response from
# the node, and performs a number of operations:
# - conversion of data items
# - update a round robin (rrdtool) database with the node data
# - periodically generate graphic charts for display in html documents
# - write the processed node status to a JSON file for use by html
# documents
#
# Copyright 2020 Jeff Owrey
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see http://www.gnu.org/license.
#
# Revision History
# * v20 released 11 Jan 2020 by J L Owrey; first release
# * v21 released 13 Feb 2020 by J L Owrey; fixed bug occuring when node
# powers on and signal data memory is empty. Data points with N/A data
# are discarded.
# * v22 released 31 Mar 2020 by J L Owrey; upgraded for compatibility with
# Aredn firmware version 3.20.3.0. This agent now downloads the node's
# status page and parsed the signal data from the html.
# * v23 released 11 Jun 2021 by J L Owrey; remove unused code.
# * v24 released 14 Jun 2021 by J L Owrey; minor revisions
# * v25 released 9 Jul 2021 by J L Owrey; improved handling of
# node status function
#
#2345678901234567890123456789012345678901234567890123456789012345678901234567890
import os
import sys
import signal
import multiprocessing
import time
import json
from urllib.request import urlopen
import rrdbase
### ENVIRONMENT ###
_USER = os.environ['USER']
_SERVER_MODE = "primary"
### DEFAULT AREDN NODE URL ###
_DEFAULT_AREDN_NODE_URL = "http://localnode.local.mesh/cgi-bin/status"
### FILE AND FOLDER LOCATIONS ###
# folder for containing dynamic data objects
_DOCROOT_PATH = "/home/%s/public_html/arednsig/" % _USER
# folder for charts and output data file
_CHARTS_DIRECTORY = _DOCROOT_PATH + "dynamic/"
# location of data output file
_OUTPUT_DATA_FILE = _DOCROOT_PATH + "dynamic/arednsigData.js"
# database that stores node data
_RRD_FILE = "/home/%s/database/arednsigData.rrd" % _USER
### GLOBAL CONSTANTS ###
# maximum number of failed data requests allowed
_MAX_FAILED_DATA_REQUESTS = 2
# maximum number of http request retries allowed
_MAX_HTTP_RETRIES = 3
# delay time between http request retries
_HTTP_RETRY_DELAY = 1.1199
# interval in seconds between data requests
_DEFAULT_DATA_REQUEST_INTERVAL = 60
# number seconds to wait for a response to HTTP request
_HTTP_REQUEST_TIMEOUT = 3
# interval in seconds between database updates
_DATABASE_UPDATE_INTERVAL = 60
# chart update interval in seconds
_CHART_UPDATE_INTERVAL = 600
# standard chart width in pixels
_CHART_WIDTH = 600
# standard chart height in pixels
_CHART_HEIGHT = 150
# Set this to True only if this server is intended to relay raw
### GLOBAL VARIABLES ###
# turn on or off of verbose debugging information
verboseMode = False
debugMode = False
reportUpdateFails = False
# The following two items are used for detecting system faults
# and aredn node online or offline status.
# count of failed attempts to get data from aredn node
failedUpdateCount = 0
httpRetries = 0
nodeOnline = False
# ip address of aredn node
arednNodeUrl = _DEFAULT_AREDN_NODE_URL
# frequency of data requests to aredn node
dataRequestInterval = _DEFAULT_DATA_REQUEST_INTERVAL
# chart update interval
chartUpdateInterval = _CHART_UPDATE_INTERVAL
# rrdtool database interface handler instance
rrdb = None
### PRIVATE METHODS ###
def getTimeStamp():
"""
Set the error message time stamp to the local system time.
Parameters: none
Returns: string containing the time stamp
"""
return time.strftime( "%m/%d/%Y %T", time.localtime() )
##end def
def setStatusToOffline():
"""Set the detected status of the aredn node to
"offline" and inform downstream clients by removing input
and output data files.
Parameters: none
Returns: nothing
"""
global nodeOnline
# Inform downstream clients by removing output data file.
if os.path.exists(_OUTPUT_DATA_FILE):
os.remove(_OUTPUT_DATA_FILE)
# If the aredn node was previously online, then send
# a message that we are now offline.
if nodeOnline:
print('%s aredn node offline' % getTimeStamp())
nodeOnline = False
##end def
def terminateAgentProcess(signal, frame):
"""Send a message to log when the agent process gets killed
by the operating system. Inform downstream clients
by removing input and output data files.
Parameters:
signal, frame - dummy parameters
Returns: nothing
"""
# Inform downstream clients by removing output data file.
if os.path.exists(_OUTPUT_DATA_FILE):
os.remove(_OUTPUT_DATA_FILE)
print('%s terminating arednsig agent process' % getTimeStamp())
sys.exit(0)
##end def
### PUBLIC METHODS ###
def getNodeData(dData):
"""Send http request to aredn node. The response from the
node contains the node signal data as unformatted ascii text.
Parameters: none
Returns: True if successful,
or False if not successful
"""
global httpRetries
try:
currentTime = time.time()
response = urlopen(arednNodeUrl, timeout=_HTTP_REQUEST_TIMEOUT)
requestTime = time.time() - currentTime
content = response.read().decode('utf-8')
content = content.replace('\n', '')
content = content.replace('\r', '')
if content == "":
raise Exception("empty response")
except Exception as exError:
# If no response is received from the device, then assume that
# the device is down or unavailable over the network. In
# that case return None to the calling function.
httpRetries += 1
if reportUpdateFails:
print("%s " % getTimeStamp(), end='')
if reportUpdateFails or verboseMode:
print("http request failed (%d): %s" % \
(httpRetries, exError))
if httpRetries > _MAX_HTTP_RETRIES:
httpRetries = 0
return False
else:
time.sleep(_HTTP_RETRY_DELAY)
return getNodeData(dData)
## end try
if debugMode:
print(content)
if verboseMode:
print("http request successful: "\
"%.4f seconds" % requestTime)
httpRetries = 0
dData['content'] = content
return True
##end def
def parseDataString(dData):
"""Parse the node signal data JSON string from the aredn node
into its component parts.
Parameters:
sData - the string containing the data to be parsed
dData - a dictionary object to contain the parsed data items
Returns: True if successful, False otherwise
"""
sData = dData.pop('content')
try:
strBeginSearch = '