### no shebang - see "On Python Shebangs" by Michael Catanzaro at https://blogs.gnome.org/mcatanzaro/2018/02/16/on-python-shebangs/
### This script will work with Python 2.7 and Python 3.5 
# coding: utf-8

import sys 

if sys.version_info[0] == 3: 
  from urllib.request import urlopen 
else: 
  from urllib2 import urlopen

from syslog import *
import argparse

### <settable default parameters>

domoticzUrl     = '192.168.0.22'
tempSensorIdx   = 0      # Domoticz idx of temperature sensor, 0 if none
tempAlertIdx    = 0      # Domoticz idx of alert sensor, 0 if none
alertTemp       = 63.0   # Hot cpu core temperature threshold
domoticzTimeout = 10     # timeout in seconds on http requests to Domoticz
logIdent        = ''     # syslog programname
loglevel        = LOG_ERR    # set to LOG_DEBUG to debug command line 
                             # parsing. After the log level is defined by
                             # the -l (--log) command line option which
                             # defaults to LOG_ERR

### </settable default parameters>

# Script version
Version = '0.2'

# Json formatted Domoticz http queries
domoticzQuery   = '/json.htm?type=command&param=udevice&idx={}&nvalue={}&svalue={}'


# Subclassing ArgumentParser and overriding its error method so that 
# parsing errors can be sent to syslog
# reference: Ned Batchlder https://stackoverflow.com/a/14728477  
class ArgumentParserError(Exception): pass

class ThrowingArgumentParser(argparse.ArgumentParser):
    def error(self, message):
        raise ArgumentParserError(message)

# parse command line arguments
ap = ThrowingArgumentParser()
ap.add_argument('-t', '--tempidx', type=int, default=tempSensorIdx, help='Domoticz IDX of temperature sensor (> 0 or 0=none)')
ap.add_argument('-a', '--alertidx', type=int, default=tempAlertIdx, help='Domoticz IDX of temp. alert sensor (> 0 or 0=none)')
ap.add_argument('-i', '--ip', type=str, default=domoticzUrl, help='Domoticz IP address as in \'http://192.168.0.22:8080\'')
ap.add_argument('-w', '--wait', type=int, default=domoticzTimeout, help='time out interval on HTTP requests in seconds (> 3)')
ap.add_argument('-d', '--danger', type=int, default=alertTemp*10, help='Temperature threshold when cpu core is dangerously hot (in tenths of °C > 200)')
ap.add_argument('-l', '--log', default='err', choices=['alert', 'err', 'debug'], help='syslog level')
ap.add_argument('-n', '--name', type=str, default=logIdent, help='syslog identifier (programname)')
ap.add_argument('-v', '--verbose', action="store_true", default=False, help='echo syslog messages to console')
ap.add_argument('-V', '--version', action='version', version='%(prog)s '+Version)

# Routine to send HTTP request to Domoticz
def sendQuery(query):
  url = domoticzUrl + query
  try:
    log(LOG_DEBUG, 'Domoticz url: {}'.format(url))

    # send the request with a timeout
    hf = urlopen(url, timeout=domoticzTimeout)

    # get Domoticz' response and broadcast it
    response = hf.read().decode('utf-8')
    if ('"ERR"') in response:
      llevel = LOG_ERR
    else:
      llevel = LOG_DEBUG
    log(llevel, 'Domoticz response: {}'.format(response))
    hf.close

  except Exception as e:
    log(LOG_ERR, 'Exception: {}'.format(e))


# Always start with verbose set to true to echo command line 
# errors to the console
verbose = True

# Routine to send messages to syslog and echo it to the console
def log(level, msg):
  if (verbose) and (level <= loglevel): print(msg)
  syslog(level, msg)

# Routine to validate that an integer command line option is above a low value
def validRange(value, low, msg, units=''):
  if value < low:
    raise ValueError("{} must be {}{} or greater".format(msg, low, units))
  return value

# Feeble attempt to get a valid IP v4 address
# will fail with IpV6
def fixupIpV4(ip):
  http_parts = ip.split('//')
  if len(http_parts) > 1:
    httpstr = http_parts[0]+'//'
    ip = http_parts[1]
  else:
    httpstr  = 'http://'
  ip_parts = ip.split('.')
  lp = len(ip_parts)-1  
  if not ':' in ip_parts[lp]:  
    ip_parts[lp] = ip_parts[lp]+':8080'
  return httpstr + '.'.join(ip_parts)

## starting script!

if logIdent:
  openlog(ident=logIdent)
  log(LOG_DEBUG, 'openlog(ident={})'.format(logIdent))

setlogmask(LOG_UPTO(loglevel)) 
log(LOG_DEBUG, 'Starting {} {}'.format(__file__, Version))

try:
  args = ap.parse_args()

  if not (logIdent == args.name):
    closelog()
    openlog(ident=args.name)
    log(LOG_DEBUG, 'closelog(), openlog(ident={})'.format(args.name))
  
  # the following can raise exceptions
  tempSensorIdx = validRange(args.tempidx, 0, 'tempidx') 
  tempAlertIdx = validRange(args.alertidx, 0, 'alertidx')
  domoticzUrl = fixupIpV4(args.ip)
  domoticzTimeout = validRange(args.wait, 4, 'wait', ' seconds')
  alertTemp = validRange(args.danger, 200, 'danger', ' tenths of ° C')/10 

  if args.log == 'alert':
    loglevel = LOG_ALERT
  elif args.log == 'debug':
    loglevel = LOG_DEBUG
  else:
    loglevel = LOG_ERR    
  verbose = args.verbose 
  setlogmask(LOG_UPTO(loglevel))  
 
except Exception as e:
  log(LOG_ERR,'Exception: {}'.format(e))
  print('Try: {} --help for more information'.format(__file__))
  #ap.print_help() ##_usage()
  quit()

'''
### <debugging and testing cli options code>
def debug_args():
  print('args: ', args)
  print('tempSensorIdx: ',tempSensorIdx)
  print('tempAlertIdx: ', tempAlertIdx) 
  print('domoticzUrl: ', domoticzUrl) 
  print('domoticzTimeout: ', domoticzTimeout) 
  print('alertTemp: ', alertTemp/10) 
  print('logIdent: \'{}\''.format(logIdent))
  print('loglevel: ', loglevel, '(ALERT=', LOG_ALERT, ', ERR=', LOG_ERR, ', DEBUG=', LOG_DEBUG, ')') 
  print('verbose: ', verbose)

debug_args()


def test_ip(src, wanted):
  fixed = fixupIpV4(src)
  if not (fixed == wanted):
    print('{} -> {}, wanted: {}'.format(src, fixed, wanted))
    return False
  else:
    return True  

ok = 0
errors = 0

sources = [ 
   ['localhost',               'http://localhost:8080'], 
   ['https://localhost',       'https://localhost:8080'], 
   ['localhost:443',           'http://localhost:443'], 
   ['http://localhost:443',    'http://localhost:443'],
    
   ['domus1.local',            'http://domus1.local:8080'],
   ['https://domus1.local',    'https://domus1.local:8080'],
   ['domus1.local:443',        'http://domus1.local:443'],
   ['http://domus1.local:00',  'http://domus1.local:00'],  ## gigo!!
   
   ['192.168.0.22',            'http://192.168.0.22:8080'],
   ['http://192.168.0.22',     'http://192.168.0.22:8080'],
   ['192.168.0.22:443',        'http://192.168.0.22:443'],
   ['https://192.168.0.22:443','https://192.168.0.22:443'],
   ]
   
for i in range(len(sources)):
  #print('src:{}, want:{}'.format(sources[i][0], sources[i][1]))
  if test_ip(sources[i][0], sources[i][1]):
    ok += 1
  else:  
    errors += 1
print('{} tests, {} passed, {} failed'.format(len(sources), ok, errors))

quit()
### <debugging and testing cli options code>
'''

# Read cpu temperature
cputemp = int(open('/sys/class/thermal/thermal_zone0/temp').read()) / 1000.0
# cputemp = 72 # to test alers
cpuTemp = "{0:0.1f}".format(cputemp)

# Send the debug message
log(LOG_DEBUG, 'CPU temperature: {}° C'.format(cpuTemp))

# Send the alert message if necessary
if cputemp > alertTemp:
  log(LOG_ALERT, 'CPU temperature {}° above threshold {}° C'.format(cpuTemp, alertTemp))


# Send the temperature to Domoticz
if tempSensorIdx > 0:
  query = domoticzQuery.format(tempSensorIdx, 0, cpuTemp)
  sendQuery(query)

# URL to send the alert to Domoticz
if tempAlertIdx > 0:
  if cputemp > alertTemp:
    value = 4 # red
  elif cputemp > int(0.9*alertTemp):
    value = 3 # orange
  elif cputemp > int(0.8*alertTemp):
    value = 2 # yellow
  else:
    value = 1 # green
  query = domoticzQuery.format(tempAlertIdx, value, 'Orange%20Pi%20Zero%20temperature:%20{}'.format(cpuTemp))
  sendQuery(query)
  
