"""
    mahali_relay_service.py

    $Id: mahali_relay_service.py 476 2015-08-13 21:20:35Z flind $

    This service acts as an interface to the tosr0x relay and mahali command and status via redis. The
    service registers its status with redis, accepts commands for LED and device power relays, and reads
    the temperature sensor at 1 Hz. State changes and temperatures are published to redis pubsub queues.
    Data is serialized into the queues as uncompressed json objects. This is transparent, will interface well
    with Java, and is appropriate for the low data rates and volumes. The internal structure of this service
    is threaded with synchronization locks to coordinate reads and writes to the underlying relay usb / serial
    interface.

"""

import time
import os
import sys
import optparse
import threading
import numpy
import json
import redis
import signal
import traceback
import daemon  # for running as a daemon
import ConfigParser

# don't normally like to import this way but coded it first and
# then moved to library. Should fixup later with better namespace
# control...
from mahali_common import *

try:
    import tosr0x  # the relay
    RELAY_MODULE = True
except ImportError:
    RELAY_MODULE = False


class relayInterface:
    """ Interface to the relay at a low level. By multiple threads with simple locking. """
    def __init__(self, logQ, options):
        self.relay_lock = threading.Lock()
        self.relay = None
        self.logQ = logQ
        self.options = options
        self.temperatures = []
        self.max_temp_cnt = 6

        self.current_power_state = 'unknown'
        self.current_led_state = 'unknown'
        self.current_temperature = -1.0E10
        self.temperature_time = -1

    def activate(self):

        if self.options.simulate:
            self.logQ.logInfo('SIMULATION MODE!')
            self.logQ.logDebug('SIMULATION MODE!')
            print "!!! Simulation Mode !!!"

        if RELAY_MODULE and not options.simulate:
            th = tosr0x.handler()
        elif self.options.simulate:
            th = ' '
            self.logQ.logDebug('simulate activate relay device')
        else:
            self.logQ.logError('No relay module and not in simulation mode. Service will exit.')
            return False

        if len(th) == 0:  # no relay found
            self.logQ.logError('No relay found. Service will exit.')
            return False
        else:
            try:
                self.relay = th[0]
            except Exception as eobj:
                exp_str = str(ExceptionString(eobj))
                emsg = "exception: %s. Could not open relay driver. Exiting." % (exp_str)
                self.logQ.logError(emsg)

        self.logQ.logInfo('relay successfully opened')

        # now force off the LED and power relay
        if RELAY_MODULE == True and not options.simulate:
            self.relay.set_relay_position(1, 0)  # relay off
            self.relay.set_relay_position(2, 0)  # LED off
        else:
            if self.options.debug:
                self.logQ.logDebug('simulate setting initial relay state')

            self.current_power_state = 'off'
            self.current_led_state = 'off'

        return True

    def deactivate(self):
        try:
            if RELAY_MODULE and not options.simulate:
                self.relay.device.close()
            elif self.options.simulate:
                if self.options.debug:
                    self.logQ.logDebug('simulate deactivate relay device')
        except Exception as eobj:
            exp_str = str(ExceptionString(eobj))
            emsg = "exception: %s. Could not deactivate driver." % (exp_str)
            self.logQ.logError(emsg)

    def getTemperature(self):
        try:
            #print "test get temperature"
            #raw = 18.0
            if RELAY_MODULE and not options.simulate:
                raw = float(self.relay.get_temperature())
            elif self.options.simulate:
                raw = 40.0 + (numpy.random.ranf() * 20.0 - 10.0)

                if self.options.debug:
                    self.logQ.logDebug('simulate read temperature with value %f' % (raw))

            else:
                self.logQ.logError('No relay module and not in simulation mode on temperature read.')


        except Exception as eobj:
            exp_str = str(ExceptionString(eobj))
            emsg = "exception: %s. Could not read temperature." % (exp_str)
            self.logQ.logError(emsg)

        self.temperatures.append(raw)
        if len(self.temperatures) > self.max_temp_cnt:
            self.temperatures.pop(0) # oldest

        mtemp = numpy.median(self.temperatures)

        self.current_temperature = mtemp
        self.temperature_time = time.time()

        if self.options.debug and self.options.verbose:
            self.logQ.logDebug('current temperature %2.1f' % float(self.current_temperature))

        return mtemp

    def setDevicePower(self, state):
        try:
            if self.options.debug and self.options.verbose:
                self.logQ.logDebug('set relay to %s state' % (state))

            if RELAY_MODULE and not options.simulate:
                self.relay.set_relay_position(1, state)
            elif self.options.simulate:
                if self.options.debug:
                    self.logQ.logDebug('simulate set device power to state %s' % (state))
            else:
                self.logQ.logError('No relay module and not in simulation mode on set device power.')

            if state == 1:
                self.current_power_state = 'on'
            else:
                  self.current_power_state = 'off'

        except Exception as eobj:
            exp_str = str(ExceptionString(eobj))
            emsg = "exception: %s. Could not set power relay to state %s." % (exp_str, state)
            self.logQ.logError(emsg)

    def setLED(self, state):
        try:
            if self.options.debug and self.options.verbose:
                self.logQ.logDebug('set led to %s state' % (state))

            if RELAY_MODULE and not options.simulate:
                self.relay.set_relay_position(2, state)
            elif self.options.simulate:
                if self.options.debug:
                    self.logQ.logDebug('simulate set led to state %s' % (state))
            else:
                self.logQ.logError('No relay module and not in simulation mode on set led.')

            if state == 1:
                self.current_led_state = 'on'
            else:
                self.current_led_state = 'off'

        except Exception as eobj:
            exp_str = str(ExceptionString(eobj))
            emsg = "exception: %s. Could not set led relay to state %s." % (exp_str, state)
            self.logQ.logError(emsg)

class TemperatureReader(KThread):
    """ Thread to read temperature periodically and publish it to a redis pubsub queue. """
    def __init__(self, relay_interface, redis_db, interval, logQ, flags, options):
        KThread.__init__(self)

        self.relay_interface = relay_interface
        self.redis_db = redis_db
        self.interval = interval
        self.logQ = logQ
        self.flags = flags
        self.options = options

    def run(self):

        if self.options.debug:
            self.logQ.logDebug('start temperature reader thread')

        while self.flags['run'].isSet():
            if self.options.debug and self.options.verbose:
                self.logQ.logDebug('top of temperature read loop at ' + str(time.gmtime(time.time())))

            time.sleep(self.interval)
            # grab relay lock
            self.relay_interface.relay_lock.acquire()

            # get temperature
            try:
                temperature_data = self.relay_interface.getTemperature()
                mtime = time.time()
            # release relay lock
            finally:
                self.relay_interface.relay_lock.release()
            # write temperature to redis

            temperature_info = {'measurement_type':'temperature', 'measurement_time':mtime, 'data':temperature_data, 'units':'deg C'}

            mahali_send_event(self.redis_db,self.logQ,'mahali-sensor-temperature','sensor','measurement', temperature_info)

            # if verbose write to standard out
            if options.debug and options.verbose:
                self.logQ.logDebug(temperature_info)


class RelayController(KThread):
    """ Thread to accept commands from a redis pubsub queue and implement the state changes. The paradigm is command input -> state change -> status output."""
    def __init__(self, relay_interface, redis_db, interval, logQ, flags, options):
        KThread.__init__(self)

        self.relay_interface = relay_interface
        self.redis_db = redis_db
        self.interval = interval # note this interval is different from the temp read interval
        self.logQ = logQ
        self.flags = flags
        self.options = options

        self.command = self.redis_db.pubsub()
        self.command.subscribe('mahali-relay-command', ignore_subscribe_messages=True)

    def publishStatus(self, cmd, result):

        # package as a dictionary
        status_info = {'command':cmd['event_data'],'status':result}
        mahali_send_event(self.redis_db,self.logQ,'mahali-relay-status','status',cmd['event'], status_info)

    def handleCommand(self, event):

        if event:
            if self.options.debug:
                self.logQ.logDebug('handle event %s' % (str(event)))
            try:
                event_time = event['unix_time']
                event_type = event['event_type']
                event_name = event['event']
                event_data = event['event_data']
            except Exception as eobj:
                exp_str = str(ExceptionString(eobj))
                emsg = "exception: %s. Unable to interpret relay controller command %s." % (exp_str, event)
                self.logQ.logError(emsg)

            if event_type == 'command':

                if event_name == 'set-power-state':
                    if event_data == 'on':
                        self.relay_interface.relay_lock.acquire()
                        try:
                            self.relay_interface.setDevicePower(1)
                            self.publishStatus(event,'power on')
                        finally:
                            self.relay_interface.relay_lock.release()

                    elif event_data == 'off':
                        self.relay_interface.relay_lock.acquire()
                        try:
                            self.relay_interface.setDevicePower(0)
                            self.publishStatus(event,'power off')
                        finally:
                            self.relay_interface.relay_lock.release()
                    else:
                        self.logQ.logError('unexpected relay controller power state %s' % (event))

                elif event_name == 'set-led-state':
                    if event_data == 'on':
                        self.relay_interface.relay_lock.acquire()
                        try:
                            self.relay_interface.setLED(1)
                            self.publishStatus(event,'LED on')
                        finally:
                            self.relay_interface.relay_lock.release()

                    elif event_data == 'off':
                        self.relay_interface.relay_lock.acquire()
                        try:
                            self.relay_interface.setLED(0)
                            self.publishStatus(event,'LED off')
                        finally:
                            self.relay_interface.relay_lock.release()
                    else:
                        self.logQ.logError('unexpected relay controller LED state %s' % (event))

                elif event_name == 'exit':
                    if event_data == 'exit':
                        self.flags['run'].clear()
                    else:
                        self.logQ.logError('unexpected relay exit command %s' % (event))

                else:
                    self.logQ.logError('unknown relay controller command %s' % (event))

            else:
                self.logQ.logError('unexpected relay controller command type %s' % (event))

        else:
            self.logQ.logError('unexpected relay controller command %s' % (event))


    def run(self):

            if self.options.debug:
                self.logQ.logInfo('start relay controller thread')

            while self.flags['run'].isSet():
                time.sleep(self.interval)

                # ensure a clean message object
                msg = None

                # non blocking wait on redis command queue
                msg = self.command.get_message()

                if self.options.debug and self.options.verbose:
                    self.logQ.logDebug('read command %s' % (str(msg)))

                if msg == None:
                    continue

                if msg:
                    if msg['type'] == 'subscribe':
                        continue

                    event = deserialize_json(msg['data'])
                # handle command
                    self.handleCommand(event)


def parse_command_line():
    parser = optparse.OptionParser()
    parser.add_option("-v", "--verbose",action="store_true", dest="verbose", default=False,help="Print status messages to stdout.")
    parser.add_option("-d", "--debug",action="store_true", dest="debug", default=False,help="Print debug messages to stdout.")
    parser.add_option("-c", "--config",dest="CPATH",help="Use configuration in path CPATH.")
    parser.add_option("-s", "--simulate",action="store_true",dest="simulate",default=False,help="Activate in simulator mode.")
    parser.add_option("-f", "--foreground",action="store_true",dest="foreground",help="Execute in foreground and not daemon context.")

    (options, args) = parser.parse_args()

    return (options, args)


# Global run flag. Simple hack to deal with the
# service architecture exit signals
flags = {'run': threading.Event()}

def sigterm_handler(signum, frame):
    global flags
    flags['run'].clear()

def mahali_relay_service(options):

    global flags

    start_time = time.time()

    # create redis interface
    redis_db = redis.StrictRedis(host='localhost', port=6379, db=0)

    # register with redis in the mahali service list
    mahali_register_service(redis_db, 'mahali-relay-service')

    # set so that we don't just exit
    flags['run'].set()

    # create logging instance
    logQ = redisLoggerQueue('relay-service',redis_db, options)

    # load configuration
    mahali_cfg = mahaliConfiguration('mahali-gps-array', options.CPATH, redis_db, logQ)

    config = mahali_cfg.load('mahali-relay-config')

    print config
    # hardwire intervals for now
    control_interval = config['mahali-relay-service']['control_interval']
    temperature_interval = config['mahali-relay-service']['temperature_interval']

    # create relay interface
    relay_interface = relayInterface(logQ, options)

    relay_interface.activate()

    # create control thread
    control_thread = RelayController(relay_interface, redis_db, control_interval, logQ, flags, options)

    # create temperature thread
    temperature_thread = TemperatureReader(relay_interface, redis_db, temperature_interval, logQ, flags, options)

    try:
        # activate threads
        control_thread.start()
        temperature_thread.start()

        # log startup
        logQ.logInfo('startup')


        # wait for exit and update redis heartbeat 1 Hz
        while flags['run'].isSet():
            time.sleep(1)

            if options.debug and options.verbose:
                logQ.logDebug('top of service loop at ' + str(time.gmtime(time.time())))

            # check for updated config and grab new values


            # push out heartbeat

            active_time = time.time() - start_time

            # data is a dictionary with key values corresponding to
            # iso8601 timestamp, unix long second timestamp (redundant), type of measurement, and data value
            mahali_send_event(redis_db,logQ,'mahali-relay-service-heartbeat','heartbeat','active',active_time)

            # mirror state
            state_info = {'start_time':start_time,
                          'active_time':active_time,
                          'update_time':time.time(),
                          'power_state':relay_interface.current_power_state,
                          'led_state':relay_interface.current_led_state,
                          'temperature':relay_interface.current_temperature,
                          'temperature_time':relay_interface.temperature_time}

            mahali_set_object(redis_db,logQ,'mahali-relay-service-state', state_info)

    except KeyboardInterrupt:
        logQ.logInfo("exiting mahali-relay-service on keyboard interrupt")

    except Exception as eobj:
        exp_str = str(ExceptionString(eobj))
        emsg = "exception: %s. Problem with mahali-relay-service." % (exp_str)
        logQ.logError(emsg)

    flags['run'].clear()

    # close the relay interface
    # note this does not shut off the device power!
    relay_interface.deactivate()

    # publish shutdown
    logQ.logInfo('shutdown')

    if not mahali_unregister_service(redis_db, 'mahali-relay-service'):
        sys.exit()

    # deregister with redis
    redis_db.connection_pool.disconnect()

    control_thread.join(0)
    temperature_thread.join(0)


if __name__ == '__main__':

    # parse command line options
    options, args = parse_command_line()

    signal.signal(signal.SIGTERM, sigterm_handler)

    if (options.foreground):
        print "Mahali Relay Service in DEBUG mode"
        mahali_relay_service(options)
    else:
    	with daemon.DaemonContext():
        	mahali_relay_service(options)
