#!/usr/bin/env python
#-------------------------------------------------------------------------------
#    FILE: controller.py
# PURPOSE: Controller Specific Detils for Base Class
#
#  AUTHOR: Jason G Yates
#    DATE: 24-Apr-2018
#
# MODIFICATIONS:
#
# USAGE: This is the base class of generator controllers. LogError or FatalError
#   should be used to log errors or fatal errors.
#
#-------------------------------------------------------------------------------

import threading, datetime, collections, os, time, json
# NOTE: collections OrderedDict is used for dicts that are displayed to the UI


from genmonlib.mysupport import MySupport
from genmonlib.mythread import MyThread
from genmonlib.mylog import SetupLogger
from genmonlib.program_defaults import ProgramDefaults

class GeneratorController(MySupport):
    #---------------------GeneratorController::__init__-------------------------
    def __init__(self,
        log,
        newinstall = False,
        simulation = False,
        simulationfile = None,
        message = None,
        feedback = None,
        config = None,
        ConfigFilePath = ProgramDefaults.ConfPath):

        super(GeneratorController, self).__init__(simulation = simulation)
        self.log = log
        self.NewInstall = newinstall
        self.Simulation = simulation
        self.SimulationFile = simulationfile
        self.FeedbackPipe = feedback
        self.MessagePipe = message
        self.config = config

        self.ModBus = None
        self.InitComplete = False
        self.IsStopping = False
        self.InitCompleteEvent = threading.Event() # Event to signal init complete
        self.CheckForAlarmEvent = threading.Event() # Event to signal checking for alarm
        self.Registers = collections.OrderedDict()         # dict for registers and values
        self.Strings = collections.OrderedDict()           # dict for registers read a string data
        self.FileData = collections.OrderedDict()          # dict for modbus file reads
        self.NotChanged = 0         # stats for registers
        self.Changed = 0            # stats for registers
        self.TotalChanged = 0.0     # ratio of changed ragisters
        self.MaintLog =  ConfigFilePath + "maintlog.json"
        self.MaintLogList = []
        self.MaintLock = threading.RLock()
        self.OutageLog = ConfigFilePath + "outage.txt"
        self.PowerLogMaxSize = 15.0       # 15 MB max size
        self.PowerLog =  ConfigFilePath + "kwlog.txt"
        self.PowerLogList = []
        self.PowerLock = threading.RLock()
        self.KWHoursMonth = None
        self.FuelMonth = None
        self.RunHoursMonth = None
        self.FuelTotal  = None
        self.LastHouseKeepingTime = None
        self.TileList = []        # Tile list for GUI
        self.TankData = None

        self.UtilityVoltsMin = 0    # Minimum reported utility voltage above threshold
        self.UtilityVoltsMax = 0    # Maximum reported utility voltage above pickup
        self.SystemInOutage = False         # Flag to signal utility power is out
        self.TransferActive = False         # Flag to signal transfer switch is allowing gen supply power
        self.ControllerSelected = None
        # The values "Unknown" are checked to validate conf file items are found
        self.FuelType = "Unknown"
        self.NominalFreq = "Unknown"
        self.NominalRPM = "Unknown"
        self.NominalKW = "Unknown"
        self.Model = "Unknown"
        self.EngineDisplacement = "Unknown"
        self.TankSize = 0
        self.UseExternalFuelData = False

        self.ProgramStartTime = datetime.datetime.now()     # used for com metrics
        self.OutageStartTime = self.ProgramStartTime        # if these two are the same, no outage has occured
        self.LastOutageDuration = self.OutageStartTime - self.OutageStartTime

        try:
            self.console = SetupLogger("controller_console", log_file = "", stream = True)
            if self.config != None:
                self.SiteName = self.config.ReadValue('sitename', default = 'Home')
                self.LogLocation = self.config.ReadValue('loglocation', default = '/var/log/')
                self.UseMetric = self.config.ReadValue('metricweather', return_type = bool, default = False)
                self.EnableDebug = self.config.ReadValue('enabledebug', return_type = bool, default = False)
                self.bDisplayUnknownSensors = self.config.ReadValue('displayunknown', return_type = bool, default = False)
                self.bDisablePowerLog = self.config.ReadValue('disablepowerlog', return_type = bool, default = False)
                self.SubtractFuel = self.config.ReadValue('subtractfuel', return_type = float, default = 0.0)
                self.UserURL = self.config.ReadValue('user_url',  default = "").strip()
                self.UseExternalFuelData = self.config.ReadValue('use_external_fuel_data', return_type = bool, default = False)

                if self.config.HasOption('outagelog'):
                    self.OutageLog = self.config.ReadValue('outagelog')

                if self.config.HasOption('kwlog'):
                    self.PowerLog = self.config.ReadValue('kwlog')

                self.PowerLogMaxSize = self.config.ReadValue('kwlogmax', return_type = float, default = 15.0)

                if self.config.HasOption('nominalfrequency'):
                    self.NominalFreq = self.config.ReadValue('nominalfrequency')
                if self.config.HasOption('nominalRPM'):
                    self.NominalRPM = self.config.ReadValue('nominalRPM')
                if self.config.HasOption('nominalKW'):
                    self.NominalKW = self.config.ReadValue('nominalKW')
                if self.config.HasOption('model'):
                    self.Model = self.config.ReadValue('model')

                if self.config.HasOption('controllertype'):
                    self.ControllerSelected = self.config.ReadValue('controllertype')

                if self.config.HasOption('fueltype'):
                    self.FuelType = self.config.ReadValue('fueltype')

                self.TankSize = self.config.ReadValue('tanksize', return_type = int, default  = 0)

                self.SmartSwitch = self.config.ReadValue('smart_transfer_switch', return_type = bool, default = False)

        except Exception as e1:
                self.FatalError("Missing config file or config file entries: " + str(e1))


    #----------  GeneratorController:StartCommonThreads-------------------------
    # called after get config file, starts threads common to all controllers
    def StartCommonThreads(self):

        self.Threads["CheckAlarmThread"] = MyThread(self.CheckAlarmThread, Name = "CheckAlarmThread")
        # start read thread to process incoming data commands
        self.Threads["ProcessThread"] = MyThread(self.ProcessThread, Name = "ProcessThread")

        if self.EnableDebug:        # for debugging registers
            self.Threads["DebugThread"] = MyThread(self.DebugThread, Name = "DebugThread")

        # start thread for kw log
        self.Threads["PowerMeter"] = MyThread(self.PowerMeter, Name = "PowerMeter")

    # ---------- GeneratorController:ProcessThread------------------------------
    #  read registers, remove items from Buffer, form packets, store register data
    def ProcessThread(self):

        try:
            self.ModBus.Flush()
            self.InitDevice()
            if self.IsStopping:
                return
            while True:
                try:
                    if not self.InitComplete:
                        self.InitDevice()
                    self.MasterEmulation()
                    if self.IsStopSignaled("ProcessThread"):
                        break
                    if self.IsStopping:
                        break
                except Exception as e1:
                    self.LogErrorLine("Error in Controller ProcessThread (1), continue: " + str(e1))
        except Exception as e1:
            self.LogErrorLine("Exiting Controller ProcessThread (2)" + str(e1))

    # ---------- GeneratorController:CheckAlarmThread---------------------------
    #  When signaled, this thread will check for alarms
    def CheckAlarmThread(self):

        time.sleep(.25)
        while True:
            try:
                if self.WaitForExit("CheckAlarmThread", 0.25):  #
                    return

                if self.CheckForAlarmEvent.is_set():
                    self.CheckForAlarmEvent.clear()
                    self.CheckForAlarms()

            except Exception as e1:
                self.LogErrorLine("Error in  CheckAlarmThread" + str(e1))

    #----------  GeneratorController:TestCommand--------------------------------
    def TestCommand(self):
        return "Not Supported"
    #----------  GeneratorController:DebugThread--------------------------------
    def DebugThread(self):

        if not self.EnableDebug:
            return
        time.sleep(.25)

        if not self.ControllerSelected == None or not len(self.ControllerSelected) or self.ControllerSelected == "generac_evo_nexus":
            MaxReg = 0x400
        else:
            MaxReg == 0x2000
        self.InitCompleteEvent.wait()

        if self.IsStopping:
            return
        self.LogError("Debug Enabled")
        self.FeedbackPipe.SendFeedback("Debug Thread Starting", FullLogs = True, Always = True, Message="Starting Debug Thread")
        TotalSent = 0

        RegistersUnderTest = collections.OrderedDict()
        RegistersUnderTestData = ""

        while True:

            if self.IsStopSignaled("DebugThread"):
                return
            if TotalSent >= 5:
                self.FeedbackPipe.SendFeedback("Debug Thread Finished", Always = True, FullLogs = True, Message="Finished Debug Thread")
                if self.WaitForExit("DebugThread", 1):  #
                    return
                continue
            try:
                for Reg in range(0x0 , MaxReg):
                    if self.WaitForExit("DebugThread", 0.25):  #
                        return
                    Register = "%04x" % Reg
                    NewValue = self.ModBus.ProcessMasterSlaveTransaction(Register, 1, skipupdate = True)
                    if not len(NewValue):
                        continue
                    OldValue = RegistersUnderTest.get(Register, "")
                    if OldValue == "":
                        RegistersUnderTest[Register] = NewValue        # first time seeing this register so add it to the list
                    elif NewValue != OldValue:
                        BitsChanged, Mask = self.GetNumBitsChanged(OldValue, NewValue)
                        RegistersUnderTestData += "Reg %s changed from %s to %s, Bits Changed: %d, Mask: %x, Engine State: %s\n" % \
                                (Register, OldValue, NewValue, BitsChanged, Mask, self.GetEngineState())
                        RegistersUnderTest[Register] = Value        # update the value

                msgbody = "\n"
                for Register, Value in RegistersUnderTest.items():
                    msgbody += self.printToString("%s:%s" % (Register, Value))

                self.FeedbackPipe.SendFeedback("Debug Thread (Registers)", FullLogs = True, Always = True, Message=msgbody, NoCheck = True)
                if len(RegistersUnderTestData):
                    self.FeedbackPipe.SendFeedback("Debug Thread (Changes)", FullLogs = True, Always = True, Message=RegistersUnderTestData, NoCheck = True)
                RegistersUnderTestData = "\n"
                TotalSent += 1

            except Exception as e1:
                self.LogErrorLine("Error in DebugThread: " + str(e1))

    #------------ GeneratorController:GetRegisterValueFromList -----------------
    def GetRegisterValueFromList(self,Register):

        return self.Registers.get(Register, "")

    #-------------GeneratorController:GetParameterBit---------------------------
    def GetParameterBit(self, Register, Mask, OnLabel = None, OffLabel = None):

        try:
            Value =  self.GetRegisterValueFromList(Register)
            if not len(Value):
                return ""

            IntValue = int(Value, 16)

            if OnLabel == None or OffLabel == None:
                return self.BitIsEqual(IntValue, Mask, Mask)
            elif self.BitIsEqual(IntValue, Mask, Mask):
                return OnLabel
            else:
                return OffLabel
        except Exception as e1:
            self.LogErrorLine("Error in GetParameterBit: " + str(e1))
            return ""

    #-------------GeneratorController:GetParameterLong--------------------------
    def GetParameterLong(self, RegisterLo, RegisterHi, Label = None, Divider = None, ReturnInt = False, ReturnFloat = False):

        try:
            if ReturnInt:
                DefaultReturn = 0
            elif ReturnFloat:
                DefaultReturn = 0.0
            else:
                DefaultReturn = ""

            if not Label == None:
                LabelStr = Label
            else:
                LabelStr = ""

            ValueLo = self.GetParameter(RegisterLo)
            ValueHi = self.GetParameter(RegisterHi)

            if not len(ValueLo) or not len(ValueHi):
                return DefaultReturn

            IntValueLo = int(ValueLo)
            IntValueHi = int(ValueHi)

            IntValue = IntValueHi << 16 | IntValueLo

            if ReturnInt:
                return IntValue

            if not Divider == None:
                FloatValue = IntValue / Divider
                if ReturnFloat:
                    return FloatValue
                return "%2.1f %s" % (FloatValue, LabelStr)
            return "%d %s" % (IntValue, LabelStr)
        except Exception as e1:
            self.LogErrorLine("Error in GetParameterBit: " + str(e1))
            return DefaultReturn

    #-------------GeneratorController:GetParameter------------------------------
    # Hex assumes no Divider and Label - return Hex string
    # ReturnInt assumes no Divier and Label - Return int
    def GetParameter(self, Register, Label = None, Divider = None, Hex = False, ReturnInt = False, ReturnFloat = False):

        try:
            if ReturnInt:
                DefaultReturn = 0
            elif ReturnFloat:
                DefaultReturn = 0.0
            else:
                DefaultReturn = ""

            Value = self.GetRegisterValueFromList(Register)
            if not len(Value):
                return DefaultReturn

            if ReturnInt:
                return int(Value,16)

            if Divider == None and Label == None:
                if Hex:
                    return Value
                elif ReturnFloat:
                    return float(int(Value,16))
                else:
                    return str(int(Value,16))

            IntValue = int(Value,16)
            if not Divider == None:
                FloatValue = IntValue / Divider
                if ReturnFloat:
                    return FloatValue
                if not Label == None:
                    return "%.2f %s" % (FloatValue, Label)
                else:
                    return "%.2f" % (FloatValue)
            elif not Label == None:
                return "%d %s" % (IntValue, Label)
            else:
                return str(int(Value,16))

        except Exception as e1:
            self.LogErrorLine("Error in GetParameter: Reg: " + Register + ": " + str(e1))
            return ""

    #---------------------GeneratorController::GetConfig------------------------
    # read conf file, used internally, not called by genmon
    # return True on success, else False
    def GetConfig(self):
        True

    #---------------------GeneratorController::SystemInAlarm--------------------
    # return True if generator is in alarm, else False
    def SystemInAlarm(self):
        return False

    #------------ GeneratorController::GetStartInfo ----------------------------
    # return a dictionary with startup info for the gui
    def GetStartInfo(self, NoTile = False):

        StartInfo = {}
        try:
            StartInfo["fueltype"] = self.FuelType
            StartInfo["model"] = self.Model
            StartInfo["nominalKW"] = self.NominalKW
            StartInfo["nominalRPM"] = self.NominalRPM
            StartInfo["nominalfrequency"] = self.NominalFreq
            StartInfo["Controller"] = "Generic Controller Name"
            StartInfo["PowerGraph"] = self.PowerMeterIsSupported()
            StartInfo["NominalBatteryVolts"] = "12"
            StartInfo["UtilityVoltageDisplayed"] = True
            StartInfo["RemoteCommands"] = True
            StartInfo["RemoteButtons"] = False

            if not NoTile:
                StartInfo["tiles"] = []
                for Tile in self.TileList:
                    StartInfo["tiles"].append(Tile.GetStartInfo())

        except Exception as e1:
            self.LogErrorLine("Error in GetStartInfo: " + str(e1))
        return StartInfo

    #------------ GeneratorController::GetStatusForGUI -------------------------
    # return dict for GUI
    def GetStatusForGUI(self):

        Status = {}
        try:
            Status["basestatus"] = self.GetBaseStatus()
            Status["switchstate"] = self.GetSwitchState()
            Status["enginestate"] = self.GetEngineState()
            Status["kwOutput"] = self.GetPowerOutput()
            Status["OutputVoltage"] = "0V"
            Status["BatteryVoltage"] = "0V"
            Status["UtilityVoltage"] = "0V"
            Status["Frequency"] = "0"
            Status["RPM"] = "0"

            # Exercise Info is a dict containing the following:
            ExerciseInfo = collections.OrderedDict()
            ExerciseInfo["Enabled"] = False
            ExerciseInfo["Frequency"] = "Weekly"    # Biweekly, Weekly or Monthly
            ExerciseInfo["Hour"] = "14"
            ExerciseInfo["Minute"] = "00"
            ExerciseInfo["QuietMode"] = "On"
            ExerciseInfo["EnhancedExerciseMode"] = False
            ExerciseInfo["Day"] = "Monday"
            Status["ExerciseInfo"] = ExerciseInfo
        except Exception as e1:
            self.LogErrorLine("Error in GetStatusForGUI: " + str(e1))
        return Status

    #---------------------GeneratorController::DisplayLogs----------------------
    def DisplayLogs(self, AllLogs = False, DictOut = False, RawOutput = False):
        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in DisplayLogs: " + str(e1))

    #------------ GeneratorController::DisplayMaintenance ----------------------
    def DisplayMaintenance (self, DictOut = False, JSONNum = False):
        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in DisplayMaintenance: " + str(e1))

    #------------ GeneratorController::DisplayStatus ---------------------------
    def DisplayStatus(self, DictOut = False, JSONNum = False):
        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in DisplayStatus: " + str(e1))

    #------------------- GeneratorController::DisplayOutage --------------------
    def DisplayOutage(self, DictOut = False, JSONNum = False):
        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in DisplayOutage: " + str(e1))

    #------------ GeneratorController::DisplayRegisters ------------------------
    def DisplayRegisters(self, AllRegs = False, DictOut = False):
        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in DisplayRegisters: " + str(e1))

    #----------  GeneratorController::SetGeneratorTimeDate----------------------
    # set generator time to system time
    def SetGeneratorTimeDate(self):

        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in SetGeneratorTimeDate: " + str(e1))

        return "Not Supported"

    #----------  GeneratorController::SetGeneratorQuietMode---------------------
    # Format of CmdString is "setquiet=yes" or "setquiet=no"
    # return  "Set Quiet Mode Command sent" or some meaningful error string
    def SetGeneratorQuietMode(self, CmdString):
        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in SetGeneratorQuietMode: " + str(e1))

        return "Not Supported"

    #----------  GeneratorController::SetGeneratorExerciseTime------------------
    # CmdString is in the format:
    #   setexercise=Monday,13:30,Weekly
    #   setexercise=Monday,13:30,BiWeekly
    #   setexercise=15,13:30,Monthly
    # return  "Set Exercise Time Command sent" or some meaningful error string
    def SetGeneratorExerciseTime(self, CmdString):
        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in SetGeneratorExerciseTime: " + str(e1))

        return "Not Supported"

    #----------  GeneratorController::SetGeneratorRemoteStartStop---------------
    # CmdString will be in the format: "setremote=start"
    # valid commands are start, stop, starttransfer, startexercise
    # return string "Remote command sent successfully" or some descriptive error
    # string if failure
    def SetGeneratorRemoteStartStop(self, CmdString):
        try:
            pass
        except Exception as e1:
            self.LogErrorLine("Error in SetGeneratorRemoteStartStop: " + str(e1))

        return "Not Supported"

    #----------  GeneratorController:GetController  ----------------------------
    # return the name of the controller, if Actual == False then return the
    # controller name that the software has been instructed to use if overridden
    # in the conf file
    def GetController(self, Actual = True):
        return "Test Controller"

    #----------  GeneratorController:ComminicationsIsActive  -------------------
    # Called every 2 seconds, if communictions are failing, return False, otherwise
    # True
    def ComminicationsIsActive(self):
        return False

    #----------  GeneratorController:ResetCommStats  ---------------------------
    # reset communication stats, normally just a call to
    #   self.ModBus.ResetCommStats() if modbus is used
    def ResetCommStats(self):
        self.ModBus.ResetCommStats()

    #----------  GeneratorController:RemoteButtonsSupported  --------------------
    # return true if Panel buttons are settable via the software
    def RemoteButtonsSupported(self):
        return False
    #----------  GeneratorController:PowerMeterIsSupported  --------------------
    # return true if GetPowerOutput is supported
    def PowerMeterIsSupported(self):
        return False

    #---------------------GeneratorController::GetPowerOutput-------------------
    # returns current kW
    # rerturn empty string ("") if not supported,
    # return kW with units i.e. "2.45kW"
    def GetPowerOutput(self, ReturnFloat = False):
        return ""

    #----------  GeneratorController:GetCommStatus  ----------------------------
    # return Dict with communication stats
    def GetCommStatus(self):
        return self.ModBus.GetCommStats()

    #------------ GeneratorController:GetRunHours ------------------------------
    def GetRunHours(self):
        return "Unknown"
    #------------ GeneratorController:GetBaseStatus ----------------------------
    # return one of the following: "ALARM", "SERVICEDUE", "EXERCISING", "RUNNING",
    # "RUNNING-MANUAL", "OFF", "MANUAL", "READY"
    def GetBaseStatus(self):
        return "OFF"

    #------------ GeneratorController:GetOneLineStatus -------------------------
    # returns a one line status for example : switch state and engine state
    def GetOneLineStatus(self):
        return "Unknown"
    #------------ GeneratorController:RegRegValue ------------------------------
    def GetRegValue(self, CmdString):

        # extract quiet mode setting from Command String
        # format is setquiet=yes or setquiet=no
        msgbody = "Invalid command syntax for command getregvalue"
        try:
            #Format we are looking for is "getregvalue=01f4"
            CmdList = CmdString.split("=")
            if len(CmdList) != 2:
                self.LogError("Validation Error: Error parsing command string in GetRegValue (parse): " + CmdString)
                return msgbody

            CmdList[0] = CmdList[0].strip()

            if not CmdList[0].lower() == "getregvalue":
                self.LogError("Validation Error: Error parsing command string in GetRegValue (parse2): " + CmdString)
                return msgbody

            Register = CmdList[1].strip()

            RegValue = self.GetRegisterValueFromList(Register)

            if RegValue == "":
                self.LogError("Validation Error: Register  not known:" + Register)
                msgbody = "Unsupported Register: " + Register
                return msgbody

            msgbody = RegValue

        except Exception as e1:
            self.LogErrorLine("Validation Error: Error parsing command string in GetRegValue: " + CmdString)
            self.LogError( str(e1))
            return msgbody

        return msgbody


    #------------ GeneratorController:ReadRegValue -----------------------------
    def ReadRegValue(self, CmdString):

        # extract quiet mode setting from Command String
        #Format we are looking for is "readregvalue=01f4"
        msgbody = "Invalid command syntax for command readregvalue"
        try:

            CmdList = CmdString.split("=")
            if len(CmdList) != 2:
                self.LogError("Validation Error: Error parsing command string in ReadRegValue (parse): " + CmdString)
                return msgbody

            CmdList[0] = CmdList[0].strip()

            if not CmdList[0].lower() == "readregvalue":
                self.LogError("Validation Error: Error parsing command string in ReadRegValue (parse2): " + CmdString)
                return msgbody

            Register = CmdList[1].strip()

            RegValue = self.ModBus.ProcessMasterSlaveTransaction( Register, 1, skipupdate = True)

            if RegValue == "":
                self.LogError("Validation Error: Register  not known (ReadRegValue):" + Register)
                msgbody = "Unsupported Register: " + Register
                return msgbody

            msgbody = RegValue

        except Exception as e1:
            self.LogErrorLine("Validation Error: Error parsing command string in ReadRegValue: " + CmdString)
            self.LogError( str(e1))
            return msgbody

        return msgbody
    #------------ GeneratorController:DisplayOutageHistory----------------------
    def DisplayOutageHistory(self):

        LogHistory = []

        if not len(self.OutageLog):
            return ""
        try:
            # check to see if a log file exist yet
            if not os.path.isfile(self.OutageLog):
                return ""

            OutageLog = []

            with open(self.OutageLog,"r") as OutageFile:     #opens file

                for line in OutageFile:
                    line = line.strip()                   # remove whitespace at beginning and end

                    if not len(line):
                        continue
                    if line[0] == "#":              # comment?
                        continue
                    Items = line.split(",")
                    # Three items is for duration greater than 24 hours, i.e 1 day, 08:12
                    if len(Items) < 2:
                        continue
                    strDuration = ""
                    strFuel = ""
                    if len(Items)  == 2:
                        # Only date and duration less than a day
                        strDuration = Items[1]
                    elif (len(Items) == 3) and ("day" in Items[1]):
                        #  date and outage greater than 24 hours
                        strDuration  = Items[1] + "," + Items[2]
                    elif len(Items) == 3:
                        # date, outage less than 1 day, and fuel
                        strDuration = Items[1]
                        strFuel = Items[2]
                    elif len(Items) == 4 and ("day" in Items[1]):
                        # date, outage less greater than 1 day, and fuel
                        strDuration = Items[1] + "," + Items[2]
                        strFuel = Items[3]
                    else:
                        continue

                    if len(strDuration)  and len(strFuel):
                        OutageLog.insert(0, [Items[0], strDuration, strFuel])
                    elif len(strDuration):
                        OutageLog.insert(0, [Items[0], strDuration])

                    if len(OutageLog) > 100:     # limit log to 100 entries
                        OutageLog.pop()

            for Items in OutageLog:
                if len(Items) == 2:
                    LogHistory.append("%s, Duration: %s" % (Items[0], Items[1]))
                elif len(Items) == 3:
                    LogHistory.append("%s, Duration: %s, Estimated Fuel: %s" % (Items[0], Items[1], Items[2]))

            return LogHistory

        except Exception as e1:
            self.LogErrorLine("Error in  DisplayOutageHistory: " + str(e1))
            return []
    #------------ GeneratorController::LogToPowerLog----------------------------
    def LogToPowerLog(self, TimeStamp, Value):

        try:
            if len(self.PowerLogList):
                self.PowerLogList.insert(0, [TimeStamp, Value])
            self.LogToFile(self.PowerLog, TimeStamp, Value)
        except Exception as e1:
            self.LogErrorLine("Error in LogToPowerLog: " + str(e1))

    #------------ GeneratorController::GetPowerLogFileDetails-------------------
    def GetPowerLogFileDetails(self):

        if not self.PowerMeterIsSupported():
            return "Not Supported"
        try:
            LogSize = os.path.getsize(self.PowerLog)
            outstr = "%.2f MB of %.2f MB" %((float(LogSize) / (1024.0*1024.0)), self.PowerLogMaxSize )
            return outstr
        except Exception as e1:
            self.LogErrorLine("Error in GetPowerLogFileDetails : " + str(e1))
            return "Unknown"
    #------------ GeneratorController::PrunePowerLog----------------------------
    def PrunePowerLog(self, Minutes):

        if not Minutes:
            self.LogError("Clearing power log")
            return self.ClearPowerLog()

        try:

            LogSize = os.path.getsize(self.PowerLog)
            if float(LogSize) / (1024*1024) < self.PowerLogMaxSize * 0.85:
                return "OK"

            if float(LogSize) / (1024*1024) >= self.PowerLogMaxSize * 0.98:
                msgbody = "The genmon kwlog (power log) file size is 98 percent of the maximum. Once "
                msgbody += "the log reaches 100 percent of the log will be reset. This will result "
                msgbody += "inaccurate fuel estimation (if you are using this feature). You can  "
                msgbody += "either increase the size of the kwlog on the advanced settings page,"
                msgbody += "or reset your power log."
                self.MessagePipe.SendMessage("Notice: Power Log file size warning" , msgbody, msgtype = "warn", onlyonce = True)

            # is the file size too big?
            if float(LogSize) / (1024*1024) >= self.PowerLogMaxSize:
                self.ClearPowerLog()
                self.LogError("Power Log entries deleted due to size reaching maximum.")
                return "OK"

            # if we get here the power log is 85% full or greater so let's try to reduce the size by
            # deleting entires that are older than the input Minutes
            CmdString = "power_log_json=%d" % Minutes
            PowerLog = self.GetPowerHistory(CmdString, NoReduce = True)

            self.ClearPowerLog(NoCreate = True)
            # Write oldest log entries first
            for Items in reversed(PowerLog):
                self.LogToPowerLog(Items[0], Items[1])

            # Add null entry at the end
            if not os.path.isfile(self.PowerLog):
                TimeStamp = datetime.datetime.now().strftime('%x %X')
                self.LogToPowerLog(TimeStamp, "0.0")

            # if the power log is now empty add one entry
            LogSize = os.path.getsize(self.PowerLog)
            if LogSize == 0:
                TimeStamp = datetime.datetime.now().strftime('%x %X')
                self.LogToPowerLog( TimeStamp, "0.0")

            return "OK"

        except Exception as e1:
            self.LogErrorLine("Error in  PrunePowerLog: " + str(e1))
            return "Error in  PrunePowerLog: " + str(e1)

    #------------ GeneratorController::ClearPowerLog----------------------------
    def ClearPowerLog(self, NoCreate = False):

        try:
            if not len(self.PowerLog):
                return "Power Log Disabled"

            if not os.path.isfile(self.PowerLog):
                return "Power Log is empty"
            try:
                with self.PowerLock:
                    os.remove(self.PowerLog)
                    time.sleep(1)
            except:
                pass

            self.PowerLogList = []

            if not NoCreate:
                # add zero entry to note the start of the log
                TimeStamp = datetime.datetime.now().strftime('%x %X')
                self.LogToPowerLog( TimeStamp, "0.0")

            return "Power Log cleared"
        except Exception as e1:
            self.LogErrorLine("Error in  ClearPowerLog: " + str(e1))
            return "Error in  ClearPowerLog: " + str(e1)

    #------------ GeneratorController::ReducePowerSamples-----------------------
    def ReducePowerSamples(self, PowerList, MaxSize):

        if MaxSize == 0:
            self.LogError("RecducePowerSamples: Error: Max size is zero")
            return []

        if len(PowerList) < MaxSize:
            self.LogError("RecducePowerSamples: Error: Can't reduce ")
            return PowerList

        try:
            Sample = int(len(PowerList) / MaxSize)
            Remain = int(len(PowerList) % MaxSize)

            NewList = []
            Count = 0
            for Count in range(len(PowerList)):
                TimeStamp, KWValue = PowerList[Count]
                if float(KWValue) == 0:
                        NewList.append([TimeStamp,KWValue])
                elif ( Count % Sample == 0 ):
                    NewList.append([TimeStamp,KWValue])

            # if we have too many entries due to a remainder or not removing zero samples, then delete some
            if len(NewList) > MaxSize:
                return self.RemovePowerSamples(NewList, MaxSize)
        except Exception as e1:
            self.LogErrorLine("Error in RecducePowerSamples: %s" % str(e1))
            return PowerList

        return NewList

    #------------ GeneratorController::RemovePowerSamples-----------------------
    def RemovePowerSamples(self, List, MaxSize):

        import random
        try:
            NewList = List[:]
            if len(NewList) <= MaxSize:
                self.LogError("RemovePowerSamples: Error: Can't remove ")
                return NewList

            Extra = len(NewList) - MaxSize
            for Count in range(Extra):
                # assume first and last sampels are zero samples so don't select thoes
                repeat = True
                while (repeat):
                    position = random.randint(1, len(NewList) - 2)
                    if float(NewList[position][1]) != 0:
                        Entry = NewList.pop(position)
                        repeat = False

            return NewList
        except Exception as e1:
            self.LogErrorLine("Error in RemovePowerSamples: %s" % str(e1))
            return NewList

    #------------ GeneratorController::GetPowerLogForMinutes--------------------
    def GetPowerLogForMinutes(self, Minutes = 0):
        try:
            ReturnList = []
            PowerList = self.ReadPowerLogFromFile()
            if not Minutes:
                return PowerList
            CurrentTime = datetime.datetime.now()

            for Time, Power in reversed(PowerList):
                struct_time = time.strptime(Time, "%x %X")
                LogEntryTime = datetime.datetime.fromtimestamp(time.mktime(struct_time))
                Delta = CurrentTime - LogEntryTime
                if self.GetDeltaTimeMinutes(Delta) < Minutes :
                    ReturnList.insert(0, [Time, Power])
            return ReturnList
        except Exception as e1:
            self.LogErrorLine("Error in GetPowerLogForMinutes: " + str(e1))
            return ReturnList

    #------------ GeneratorController::ReadPowerLogFromFile---------------------
    def ReadPowerLogFromFile(self, Minutes = 0, NoReduce = False):

        # check to see if a log file exist yet
        if not os.path.isfile(self.PowerLog):
            return []
        PowerList = []
        CurrentTime = datetime.datetime.now()

        # return cached list if we have read the file before
        if len(self.PowerLogList) and not Minutes:
            return self.PowerLogList
        with self.PowerLock:
            if Minutes:
                return self.GetPowerLogForMinutes(Minutes)

            try:
                with open(self.PowerLog,"r") as LogFile:     #opens file
                    for line in LogFile:
                        line = line.strip()                  # remove whitespace at beginning and end

                        if not len(line):
                            continue
                        if line[0] == "#":                  # comment
                            continue
                        Items = line.split(",")
                        if len(Items) != 2:
                            continue
                        # remove any kW labels that may be there
                        Items[1] = self.removeAlpha(Items[1])
                        PowerList.insert(0, [Items[0], Items[1]])

            except Exception as e1:
                self.LogErrorLine("Error in  ReadPowerLogFromFile (parse file): " + str(e1))

            if len(PowerList) > 500 and not NoReduce:
                PowerList = self.ReducePowerSamples(PowerList, 500)
            if not len(self.PowerLogList):
                self.PowerLogList = PowerList
        return PowerList
    #------------ GeneratorController::GetPowerHistory--------------------------
    def GetPowerHistory(self, CmdString, NoReduce = False):

        KWHours = False
        FuelConsumption = False
        RunHours = False
        msgbody = "Invalid command syntax for command power_log_json"

        try:
            if not len(self.PowerLog):
                # power log disabled
                return []

            if not len(CmdString):
                self.LogError("Error in GetPowerHistory: Invalid input")
                return []

            #Format we are looking for is "power_log_json=5" or "power_log_json" or "power_log_json=1000,kw"
            CmdList = CmdString.split("=")

            if len(CmdList) > 2:
                self.LogError("Validation Error: Error parsing command string in GetPowerHistory (parse): " + CmdString)
                return msgbody

            CmdList[0] = CmdList[0].strip()

            if not CmdList[0].lower() == "power_log_json":
                self.LogError("Validation Error: Error parsing command string in GetPowerHistory (parse2): " + CmdString)
                return msgbody

            if len(CmdList) == 2:
                ParseList = CmdList[1].split(",")
                if len(ParseList) == 1:
                    Minutes = int(CmdList[1].strip())
                elif len(ParseList) == 2:
                    Minutes = int(ParseList[0].strip())
                    if ParseList[1].strip().lower() == "kw":
                        KWHours = True
                    elif ParseList[1].strip().lower() == "fuel":
                        FuelConsumption = True
                    elif ParseList[1].strip().lower() == "time":
                        RunHours = True
                else:
                    self.LogError("Validation Error: Error parsing command string in GetPowerHistory (parse3): " + CmdString)
                    return msgbody

            else:
                Minutes = 0
        except Exception as e1:
            self.LogErrorLine("Error in  GetPowerHistory (Parse): %s : %s" % (CmdString,str(e1)))
            return msgbody

        try:

            PowerList = self.ReadPowerLogFromFile( Minutes = Minutes)

            #Shorten list to 500 if specific duration requested
            #if not KWHours and len(PowerList) > 500 and Minutes and not NoReduce:
            if len(PowerList) > 500 and Minutes and not NoReduce:
                PowerList = self.ReducePowerSamples(PowerList, 500)

            if KWHours:
                AvgPower, TotalSeconds = self.GetAveragePower(PowerList)
                return "%.2f" % ((TotalSeconds / 3600) * AvgPower)
            if FuelConsumption:
                AvgPower, TotalSeconds = self.GetAveragePower(PowerList)
                Consumption, Label = self.GetFuelConsumption(AvgPower, TotalSeconds)
                if Consumption == None:
                    return "Unknown"
                return "%.2f %s" % (Consumption, Label)
            if RunHours:
                AvgPower, TotalSeconds = self.GetAveragePower(PowerList)
                return "%.2f" % (TotalSeconds / 60.0 / 60.0)

            return PowerList

        except Exception as e1:
            self.LogErrorLine("Error in  GetPowerHistory: " + str(e1))
            msgbody = "Error in  GetPowerHistory: " + str(e1)
            return msgbody

    #----------  GeneratorController::GetAveragePower---------------------------
    # a list of the power log is passed in (already parsed for a time period)
    # returns a time period and average power used for that time period
    def GetAveragePower(self, PowerList):

        try:
            TotalTime = datetime.timedelta(seconds=0)
            Entries = 0
            TotalPower = 0.0
            LastPower = 0.0
            LastTime = None
            for Items in PowerList:
                Power = float(Items[1])
                struct_time = time.strptime(Items[0], "%x %X")
                LogEntryTime = datetime.datetime.fromtimestamp(time.mktime(struct_time))

                if LastTime != None:
                    if LogEntryTime > LastTime:
                        self.LogError("Error in GetAveragePower: time sequence error")

                if LastTime == None or Power == 0:
                    TotalTime += LogEntryTime - LogEntryTime
                else:
                    TotalTime += LastTime - LogEntryTime
                    TotalPower += (Power + LastPower) / 2
                    Entries += 1
                LastTime = LogEntryTime
                LastPower = Power

            if Entries == 0:
                return 0,0
            TotalPower = TotalPower / Entries
            return TotalPower, TotalTime.total_seconds()
        except Exception as e1:
            self.LogErrorLine("Error in  GetAveragePower: " + str(e1))
            return 0, 0

    #----------  GeneratorController::PowerMeter--------------------------------
    #----------  Monitors Power Output
    def PowerMeter(self):

        # make sure system is up and running otherwise we will not know which controller is present
        time.sleep(1)
        while True:

            if self.InitComplete:
                break
            if self.WaitForExit("PowerMeter", 1):
                return

        # if power meter is not supported do nothing.
        # Note: This is done since if we killed the thread here
        while not self.PowerMeterIsSupported() or not len(self.PowerLog):
            if self.WaitForExit("PowerMeter", 60):
                return

        # if log file is empty or does not exist, make a zero entry in log to denote start of collection
        if not os.path.isfile(self.PowerLog) or os.path.getsize(self.PowerLog) == 0:
            TimeStamp = datetime.datetime.now().strftime('%x %X')
            self.LogError("Creating Power Log: " + self.PowerLog)
            self.LogToPowerLog( TimeStamp, "0.0")

        LastValue = 0.0
        LastPruneTime = datetime.datetime.now()
        LastFuelCheckTime = datetime.datetime.now()
        while True:
            try:
                if self.WaitForExit("PowerMeter", 10):
                    return

                # Housekeeping on kw Log
                if LastValue == 0:
                    if self.GetDeltaTimeMinutes(datetime.datetime.now() - LastPruneTime) > 1440 :     # check every day
                        self.PrunePowerLog(60 * 24 * 30 * 36)   # delete log entries greater than three years
                        LastPruneTime = datetime.datetime.now()

                if self.GetDeltaTimeMinutes(datetime.datetime.now() - LastFuelCheckTime) > 10 :         # check 10 min
                    LastFuelCheckTime = datetime.datetime.now()
                    self.CheckFuelLevel()

                # Time to exit?
                if self.IsStopSignaled("PowerMeter"):
                    return
                KWFloat = self.GetPowerOutput(ReturnFloat = True)

                if LastValue == KWFloat:
                    continue

                if LastValue == 0:
                    StartTime = datetime.datetime.now() - datetime.timedelta(seconds=1)
                    TimeStamp = StartTime.strftime('%x %X')
                    self.LogToPowerLog( TimeStamp, str(LastValue))

                LastValue = KWFloat
                # Log to file
                TimeStamp = datetime.datetime.now().strftime('%x %X')
                self.LogToPowerLog( TimeStamp, str(KWFloat))

            except Exception as e1:
                self.LogErrorLine("Error in PowerMeter: " + str(e1))

    #----------  GeneratorController::GetFuelLevel------------------------------
    def GetFuelLevel(self, ReturnFloat = False):
        # return 0 - 100 or None

        if not self.FuelConsumptionGaugeSupported():
            return None
        if not self.FuelTankCalculationSupported() and not self.FuelSensorSupported():
            return None

        if self.FuelSensorSupported():
            FuelLevel = float(self.GetFuelSensor(ReturnInt = True))
        else:
            if self.TankSize == 0:
                return None
            FuelInTank = self.GetEstimatedFuelInTank(ReturnFloat = True)

            if FuelInTank >= self.TankSize:
                FuelLevel = 100
            else:
                FuelLevel = float(FuelInTank) / float(self.TankSize) * 100

        if ReturnFloat:
            return float(FuelLevel)
        else:
            return "%.2f %%" % FuelLevel
    #----------  GeneratorController::CheckFuelLevel----------------------------
    def CheckFuelLevel(self):
        try:
            if not self.FuelConsumptionGaugeSupported():
                return True
            if not self.FuelTankCalculationSupported() and not self.FuelSensorSupported():
                return True

            FuelLevel = self.GetFuelLevel(ReturnFloat = True)

            if FuelLevel == None:
                return True

            if FuelLevel <= 10:    # Ten percent left
                msgbody = "Warning: The estimated fuel in the tank is at or below 10%"
                title = "Warning: Fuel Level Low (10%) at " + self.SiteName
                self.MessagePipe.SendMessage(title , msgbody, msgtype = "warn", onlyonce = True)
                return False
            elif FuelLevel <= 20:    # 20 percent left
                msgbody = "Warning: The estimated fuel in the tank is at or below 20%"
                title = "Warning: Fuel Level Low (20%) at " + self.SiteName
                self.MessagePipe.SendMessage(title , msgbody, msgtype = "warn", onlyonce = True)
                return False
            else:
                return True

        except Exception as e1:
            self.LogErrorLine("Error in CheckFuelLevel: " + str(e1))
            return True
    #----------  GeneratorController::GetEstimatedFuelInTank--------------------
    def GetEstimatedFuelInTank(self, ReturnFloat = False):

        if ReturnFloat:
            DefaultReturn = 0.0
        else:
            DefaultReturn = "0"

        if not self.FuelConsumptionGaugeSupported():
            return DefaultReturn
        if not self.FuelTankCalculationSupported():
            return DefaultReturn

        if self.TankSize == 0:
            return DefaultReturn
        try:
            FuelUsed = self.GetPowerHistory("power_log_json=0,fuel")
            if FuelUsed == "Unknown" or not len(FuelUsed):
                return DefaultReturn
            FuelUsed = self.removeAlpha(FuelUsed)
            FuelLeft = float(self.TankSize) - float(FuelUsed)
            FuelLeft = float(FuelLeft) - float(self.SubtractFuel)

            if FuelLeft < 0:
                FuelLeft = 0.0
            if self.UseMetric:
                Units = "L"
            else:
                Units = "gal"
            if ReturnFloat:
                return FuelLeft
            return "%.2f %s" % (FuelLeft, Units)
        except Exception as e1:
            self.LogErrorLine("Error in GetEstimatedFuelInTank: " + str(e1))
            return DefaultReturn

    #------------ Evolution:GetFuelSensor --------------------------------------
    def GetFuelSensor(self, ReturnInt = False):
        return None
    #----------  GeneratorController::FuelSensorSupported------------------------
    def FuelSensorSupported(self):
        return False
    #----------  GeneratorController::FuelTankCalculationSupported------------------
    def FuelTankCalculationSupported(self):
        return False
    #----------  GeneratorController::FuelConsumptionSupported------------------
    def FuelConsumptionSupported(self):
        return False
    #----------  GeneratorController::FuelConsumptionGaugeSupported-------------
    def FuelConsumptionGaugeSupported(self):
        return False

    #----------  GeneratorController::GetFuelConsumption------------------------
    def GetFuelConsumption(self, kw, seconds):
        try:
            Polynomial = self.GetFuelConsumptionPolynomial()
            if Polynomial == None or len(Polynomial) != 4:
                return None, ""

            Load = kw / int(self.NominalKW)
            # Consumption of load for 1 hour
            Consumption = (Polynomial[0] * (Load ** 2)) + (Polynomial[1] * Load) + Polynomial[2]

            # now compensate for time
            Consumption = (seconds / 3600) * Consumption

            if self.UseMetric:
                if self.FuelType == "Natural Gas":
                    Consumption = Consumption * 0.0283168   # cubic feet to cubic meters
                    return round(Consumption, 4), "cubic meters"       # convert to Liters
                else:
                    Consumption = Consumption * 3.78541     # gal to liters
                    return round(Consumption, 4), "L"       # convert to Liters
            else:
                return round(Consumption, 4), Polynomial[3]
        except Exception as e1:
            self.LogErrorLine("Error in GetFuelConsumption: " + str(e1))
            return None, ""
    #----------  GeneratorController::GetFuelConsumptionPolynomial--------------
    def GetFuelConsumptionPolynomial(self):
        return None
    #----------  GeneratorController::ExternalFuelDataSupported-----------------
    def ExternalFuelDataSupported(self):
        return self.UseExternalFuelData
    #----------  GeneratorController::GetExternalFuelPercentage-----------------
    def GetExternalFuelPercentage(self, ReturnFloat = False):

        try:
            if ReturnFloat:
                DefaultReturn = 0.0
            else:
                DefaultReturn = "0"

            if not self.ExternalFuelDataSupported():
                return DefaultReturn

            if self.TankData != None:
                percentage =  self.TankData["Percentage"]
                if ReturnFloat:
                    return float(percentage)
                else:
                    return str(percentage)
            else:
                return DefaultReturn
        except Exception as e1:
            self.LogErrorLine("Error in GetExternalFuelPercentage: " + str(e1))
            return DefaultReturn
    #----------  GeneratorController::SetExternalTankData-----------------------
    def SetExternalTankData(self, command):

        try:
            CmdList = command.split("=")
            if len(CmdList) == 2:
                self.TankData = json.loads(CmdList[1])
            else:
                self.LogError("Error in  SetExternalTankData: invalid input")
                return "Error"
        except Exception as e1:
            self.LogErrorLine("Error in SetExternalTankData: " + str(e1))
            return "Error"
        return "OK"
    #----------  GeneratorController::AddEntryToMaintLog------------------------
    def AddEntryToMaintLog(self, InputString):

        ValidInput = False
        EntryString = InputString
        if EntryString == None or not len(EntryString):
            return "Invalid input for Maintenance Log entry."

        EntryString = EntryString.strip()
        if EntryString.startswith("add_maint_log"):
            EntryString = EntryString[len('add_maint_log'):]
            EntryString = EntryString.strip()
            if EntryString.strip().startswith("="):
                EntryString = EntryString[len("="):]
                EntryString = EntryString.strip()
                ValidInput = True

        if ValidInput:
            try:
                Entry = json.loads(EntryString)
                # validate object
                if not self.ValidateMaintLogEntry(Entry):
                    return "Invalid maintenance log entry"
                self.MaintLogList.append(Entry)
                with open(self.MaintLog, 'w') as outfile:
                    json.dump(self.MaintLogList, outfile, sort_keys = True, indent = 4) #, ensure_ascii = False)
            except Exception as e1:
                self.LogErrorLine("Error in AddEntryToMaintLog: " + str(e1))
                return "Invalid input for Maintenance Log entry (2)."
        else:
            self.LogError("Error in AddEntryToMaintLog: invalid input: " + str(InputString))
            return "Invalid input for Maintenance Log entry (3)."
        return "OK"

    #----------  GeneratorController::ValidateMaintLogEntry---------------------
    def ValidateMaintLogEntry(self, Entry):

        try:
            # add_maint_log={"date":"01/02/2019 14:59", "type":"Repair", "comment":"Hello"}
            if not isinstance(Entry, dict):
                self.LogError("Error in ValidateMaintLogEntry: Entry is not a dict")
                return False

            if not isinstance(Entry["date"], str) and not isinstance(Entry["date"], unicode):
                self.LogError("Error in ValidateMaintLogEntry: Entry date is not a string: " + str(type(Entry["date"])))
                return False

            try:
                EntryDate = datetime.datetime.strptime(Entry["date"], "%m/%d/%Y %H:%M")
            except Exception as e1:
                self.LogErrorLine("Error in ValidateMaintLogEntry: expecting MM/DD/YYYY : " + str(e1))

            if not isinstance(Entry["type"], str) and not isinstance(Entry["type"], unicode):
                self.LogError("Error in ValidateMaintLogEntry: Entry type is not a string: " + str(type(Entry["hours"])))
                return False
            if not Entry["type"].lower() in ["maintenance", "check", "repair", "observation"]:
                self.LogError("Error in ValidateMaintLogEntry: Invalid type: " + str(Entry["type"]))

            Entry["type"] = Entry["type"].title()

            if not isinstance(Entry["hours"], int) and not isinstance(Entry["hours"], float) :
                self.LogError("Error in ValidateMaintLogEntry: Entry type is not a number: " + str(type(Entry["hours"])))
                return False
            if not isinstance(Entry["comment"], str) and not isinstance(Entry["comment"], unicode):
                self.LogError("Error in ValidateMaintLogEntry: Entry comment is not a string: " + str(type(Entry["comment"])))

        except Exception as e1:
            self.LogErrorLine("Error in ValidateMaintLogEntry: " + str(e1))
            return False

        return True
    #----------  GeneratorController::GetMaintLog-------------------------------
    def GetMaintLog(self):

        try:
            if len(self.MaintLogList):
                return json.dumps(self.MaintLogList)
            if os.path.isfile(self.MaintLog):
                try:
                    with open(self.MaintLog) as infile:
                        self.MaintLogList = json.load(infile)
                        return json.dumps(self.MaintLogList)
                except Exception as e1:
                    self.LogErrorLine("Error in GetMaintLog: " + str(e1))
        except Exception as e1:
            self.LogErrorLine("Error in GetMaintLog (2): " + str(e1))

        return "[]"

    #----------  GeneratorController::ClearMaintLog-------------------------------
    def ClearMaintLog(self):
        try:
            if len(self.MaintLog) and os.path.isfile(self.MaintLog):
                try:
                    with self.MaintLock:
                        os.remove(self.MaintLog)
                except:
                    pass

            self.MaintLogList = []

            return "Maintenance Log cleared"
        except Exception as e1:
            self.LogErrorLine("Error in  ClearMaintLog: " + str(e1))
            return "Error in  ClearMaintLog: " + str(e1)

    #----------  GeneratorController::Close-------------------------------------
    def Close(self):

        try:
            # Controller
            self.IsStopping = True
            try:
                self.InitCompleteEvent.set()
            except:
                pass

            if self.ModBus != None:
                try:
                    self.ModBus.Close()
                except:
                    pass
            try:
                if self.EnableDebug:
                    self.KillThread("DebugThread")
            except:
                pass

            try:
                self.KillThread("ProcessThread")
            except:
                pass

            try:
                self.KillThread("CheckAlarmThread")
            except:
                pass

            try:
                self.KillThread("PowerMeter")
            except:
                pass

        except Exception as e1:
            self.LogErrorLine("Error Closing Controller: " + str(e1))

        with self.CriticalLock:
            self.InitComplete = False