#!/usr/bin/env python #------------------------------------------------------------------------------- # FILE: genexercise.py # PURPOSE: genexercise.py add enhanced exercise functionality for Evolution and Nexus cotrollers # # AUTHOR: jgyates # DATE: 02-23-2019 # # MODIFICATIONS: #------------------------------------------------------------------------------- import datetime, time, sys, signal, os, threading, collections, json, ssl import atexit, getopt try: from genmonlib.myclient import ClientInterface from genmonlib.mylog import SetupLogger from genmonlib.myconfig import MyConfig from genmonlib.mysupport import MySupport from genmonlib.mycommon import MyCommon from genmonlib.mythread import MyThread from genmonlib.program_defaults import ProgramDefaults except Exception as e1: print("\n\nThis program requires the modules located in the genmonlib directory in the github repository.\n") print("Please see the project documentation at https://github.com/jgyates/genmon.\n") print("Error: " + str(e1)) sys.exit(2) #------------ GenExercise class ------------------------------------------------ class GenExercise(MySupport): #------------ GenExercise::init--------------------------------------------- def __init__(self, log = None, loglocation = ProgramDefaults.LogPath, ConfigFilePath = MyCommon.DefaultConfPath, host = ProgramDefaults.LocalHost, port = ProgramDefaults.ServerPort): super(GenExercise, self).__init__() self.LogFileName = loglocation + "genexercise.log" self.AccessLock = threading.Lock() # log errors in this module to a file self.log = SetupLogger("genexercise", self.LogFileName) self.console = SetupLogger("genexercise_console", log_file = "", stream = True) self.MonitorAddress = host self.PollTime = 2 self.ExerciseActive = False self.Debug = False try: self.config = MyConfig(filename = ConfigFilePath + 'genexercise.conf', section = 'genexercise', log = self.log) self.ExerciseType = self.config.ReadValue('exercise_type', default = "Normal") self.ExerciseHour = self.config.ReadValue('exercise_hour', return_type = int, default = 12) self.ExerciseMinute = self.config.ReadValue('exercise_minute', return_type = int, default = 0) self.ExerciseDayOfMonth = self.config.ReadValue('exercise_day_of_month', return_type = int, default = 1) self.ExerciseDayOfWeek = self.config.ReadValue('exercise_day_of_week', default = "Monday") self.ExerciseDuration = self.config.ReadValue('exercise_duration', return_type = float, default = 12) self.ExerciseWarmup = self.config.ReadValue('exercise_warmup', return_type = float, default = 0) self.ExerciseFrequency = self.config.ReadValue('exercise_frequency', default = "Monthly") self.MonitorAddress = self.config.ReadValue('monitor_address', default = ProgramDefaults.LocalHost) self.LastExerciseTime = self.config.ReadValue('last_exercise', default = None) self.UseGeneratorTime = self.config.ReadValue('use_gen_time', return_type = bool, default = False) self.Debug = self.config.ReadValue('debug', return_type = bool, default = False) # Validate settings if not self.ExerciseType.lower() in ["normal","quiet", "transfer"]: self.ExerciseType = "normal" if self.ExerciseHour > 23 or self.ExerciseHour < 0: self.ExerciseHour = 12 if self.ExerciseMinute > 59 or self.ExerciseMinute < 0: self.ExerciseMinute = 0 if not self.ExerciseDayOfWeek.lower() in ["monday", "tuesday", "wednesday","thursday","friday","saturday","sunday"]: self.ExerciseDayOfWeek = "Monday" if self.ExerciseDayOfMonth > 28 or self.ExerciseDayOfMonth < 1: self.ExerciseDayOfMonth = 1 if self.ExerciseDuration > 60: self.ExerciseDuration = 60 if self.ExerciseDuration < 5: self.ExerciseDuration = 5 if self.ExerciseWarmup > 30: self.ExerciseWarmup = 30 if self.ExerciseWarmup < 0: self.ExerciseWarmup = 0 if not self.ExerciseFrequency.lower() in ["weekly","biweekly", "monthly"]: self.ExerciseFrequency = "Monthly" if self.MonitorAddress == None or not len(self.MonitorAddress): self.MonitorAddress = ProgramDefaults.LocalHost except Exception as e1: self.LogErrorLine("Error reading " + ConfigFilePath + "genexercise.conf: " + str(e1)) self.console.error("Error reading " + ConfigFilePath + "genexercise.conf: " + str(e1)) sys.exit(1) try: try: startcount = 0 while startcount <= 10: try: self.Generator = ClientInterface(host = self.MonitorAddress, port = port, log = self.log) break except Exception as e1: startcount += 1 if startcount >= 10: self.console.info("genmon not loaded.") self.LogError("Unable to connect to genmon.") sys.exit(1) time.sleep(1) continue except Exception as e1: self.LogErrorLine("Error in genexercise init: " + str(e1)) if not self.CheckGeneratorRequirement(): self.LogError("Requirements not met. Exiting.") sys.exit(1) # start thread monitor time for exercise self.Threads["ExerciseThread"] = MyThread(self.ExerciseThread, Name = "ExerciseThread", start = False) self.Threads["ExerciseThread"].Start() try: if self.ExerciseFrequency.lower() == "monthly": DayStr = "Day " + str(self.ExerciseDayOfMonth) else: DayStr = str(self.ExerciseDayOfWeek) self.LogError("Execise: " + self.ExerciseType + ", " + self.ExerciseFrequency + " at " + str(self.ExerciseHour) + ":" + str(self.ExerciseMinute) + " on " + DayStr + " for " + str(self.ExerciseDuration) + " min. Warmup: " + str(self.ExerciseWarmup)) self.DebugOutput("Debug Enabled") except Exception as e1: self.LogErrorLine(str(e1)) atexit.register(self.Close) signal.signal(signal.SIGTERM, self.Close) signal.signal(signal.SIGINT, self.Close) except Exception as e1: self.LogErrorLine("Error in GenExercise init: " + str(e1)) self.console.error("Error in GenExercise init: " + str(e1)) sys.exit(1) #---------- GenExercise::SendCommand -------------------------------------- def SendCommand(self, Command): if len(Command) == 0: return "Invalid Command" try: with self.AccessLock: data = self.Generator.ProcessMonitorCommand(Command) except Exception as e1: self.LogErrorLine("Error calling ProcessMonitorCommand: " + str(Command)) data = "" return data #---------- GenExercise::CheckGeneratorRequirement ------------------------ def CheckGeneratorRequirement(self): try: data = self.SendCommand("generator: start_info_json") StartInfo = {} StartInfo = json.loads(data) if not "evolution" in StartInfo["Controller"].lower() and not "nexus" in StartInfo["Controller"].lower(): self.LogError("Error: Only Evolution or Nexus controllers are supported for this feature: " + StartInfo["Controller"]) return False return True except Exception as e1: self.LogErrorLine("Error in CheckGeneratorRequirement: " + str(e1)) return False # ---------- GenExercise::PostWarmup---------------------------------------- def PostWarmup(self): # check to see if the generator is running status = self.SendCommand("generator: getbase") if not status.lower() in ["running", "exercising"]: self.LogError("WARNING: generator not running post warmup. Transfer switch not activated.") self.SendCommand("generator: setremote=stop") return self.SendCommand("generator: setremote=starttransfer") self.DebugOutput("Starting transfer exercise cycle (post warmup).") # set timer to stop self.StopTimer = threading.Timer(float(self.ExerciseDuration * 60.0), self.StopExercise) self.StopTimer.start() # ---------- GenExercise::ReadyToExercise----------------------------------- def ReadyToExercise(self): status = self.SendCommand("generator: getbase") if not status.lower() in ["ready"]: self.LogError("Generator not in Ready state, exercise cycle not started") return False return True # ---------- GenExercise::StartExercise------------------------------------- def StartExercise(self): if self.ExerciseActive: # already active return # Start generator if self.ExerciseType.lower() == "normal" and self.ReadyToExercise(): self.SendCommand("generator: setremote=start") self.DebugOutput("Starting normal exercise cycle.") self.StopTimer = threading.Timer(float(self.ExerciseDuration * 60.0), self.StopExercise) self.StopTimer.start() elif self.ExerciseType.lower() == "quiet" and self.ReadyToExercise(): self.SendCommand("generator: setremote=startexercise") self.DebugOutput("Starting quiet exercise cycle.") self.StopTimer = threading.Timer(float(self.ExerciseDuration * 60.0), self.StopExercise) self.StopTimer.start() elif self.ExerciseType.lower() == "transfer" and self.ReadyToExercise(): if self.ExerciseWarmup == 0: self.SendCommand("generator: setremote=starttransfer") self.DebugOutput("Starting transfer exercise cycle.") self.StopTimer = threading.Timer(float(self.ExerciseDuration * 60.0), self.StopExercise) self.StopTimer.start() else: self.SendCommand("generator: setremote=start") self.DebugOutput("Starting warmup for transfer exercise cycle.") # start timer for post warmup transition to starttransfer command self.WarmupTimer = threading.Timer(float(self.ExerciseWarmup * 60.0), self.PostWarmup) self.WarmupTimer.start() else: self.LogError("Invalid mode in StartExercise: " + str(self.ExerciseType)) return self.WriteLastExerciseTime() self.ExerciseActive = True # ---------- GenExercise::StopExercise-------------------------------------- def StopExercise(self): if self.ExerciseActive: self.SendCommand("generator: setremote=stop") self.DebugOutput("Stopping exercise cycle.") self.ExerciseActive = False else: self.DebugOutput("Calling Stop Exercise (not needed)") # ---------- GenExercise::DebugOutput----------------------------- def DebugOutput(self, Message): if self.Debug: self.LogError(Message) # ---------- GenExercise::WriteLastExerciseTime----------------------------- def WriteLastExerciseTime(self): try: NowString = datetime.datetime.now().strftime("%A %B %d, %Y %H:%M:%S") if self.ExerciseFrequency.lower() == "biweekly": self.config.WriteValue("last_exercise", NowString) self.config.LastExerciseTime = NowString self.DebugOutput("Last Exercise Cycle: " + NowString) except Exception as e1: self.LogErrorLine("Error in WriteLastExerciseTime: " + str(e1)) # ---------- GenExercise::TimeForExercise----------------------------------- def TimeForExercise(self): try: if self.UseGeneratorTime: TimeNow = self.GetGeneratorTime() else: TimeNow = datetime.datetime.now() if TimeNow.hour != self.ExerciseHour or TimeNow.minute != self.ExerciseMinute: return False weekDays = ("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday") WeekDayString = weekDays[TimeNow.weekday()] if not self.ExerciseFrequency.lower() in ["weekly", "biweekly", "monthly"]: self.LogError("Invalid Exercise Frequency in TimeForExercise: " + str(self.ExerciseFrequency)) return False if self.ExerciseFrequency.lower() == "weekly" and self.ExerciseDayOfWeek.lower() == WeekDayString.lower(): return True elif self.ExerciseFrequency.lower() == "biweekly" and self.ExerciseDayOfWeek.lower() == WeekDayString.lower(): if self.LastExerciseTime == None: return True LastExerciseTime = datetime.datetime.strptime(self.LastExerciseTime, "%A %B %d, %Y %H:%M:%S") if (TimeNow - LastExerciseTime).days >= 14: return True return False elif self.ExerciseFrequency.lower() == "monthly" and TimeNow.day == self.ExerciseDayOfMonth: return True else: return False except Exception as e1: self.LogErrorLine("Error in TimeForExercise: " + str(e1)) return False # ---------- GenExercise::GetGeneratorTime---------------------------------- def GetGeneratorTime(self): try: GenTimeStr = "" data = self.SendCommand("generator: status_json") Status = {} Status = json.loads(data) TimeDict = self.FindDictValueInListByKey("Time", Status["Status"]) if TimeDict != None: TimeDictStr = self.FindDictValueInListByKey("Generator Time", TimeDict) if TimeDictStr != None or not len(TimeDictStr): GenTimeStr = TimeDictStr # Format is "Wednesday March 6, 2019 13:10" or " "Friday May 3, 2019 11:11" GenTime = datetime.datetime.strptime(GenTimeStr, "%A %B %d, %Y %H:%M") else: self.LogError("Error getting generator time! Genmon may be starting up.") GenTime = datetime.datetime.now() else: self.LogError("Error getting generator time (2)!") GenTime = datetime.datetime.now() return GenTime except Exception as e1: self.LogErrorLine("Error in GetGeneratorTime: " + str(e1) + ": " + GenTimeStr) return datetime.datetime.now() # ---------- GenExercise::ExerciseThread------------------------------------ def ExerciseThread(self): time.sleep(1) while True: try: if not self.ExerciseActive: if self.TimeForExercise(): self.StartExercise() if self.WaitForExit("ExerciseThread", float(self.PollTime)): return except Exception as e1: self.LogErrorLine("Error in ExerciseThread: " + str(e1)) if self.WaitForExit("ExerciseThread", float(self.PollTime)): return # ----------GenExercise::Close---------------------------------------------- def Close(self): self.KillThread("ExerciseThread") if self.ExerciseActive: try: self.WarmupTimer.cancel() except: pass try: self.StopTimer.cancel() except: pass self.StopExercise() self.Generator.Close() #------------------------------------------------------------------------------- if __name__ == "__main__": console = SetupLogger("genexerciselog_console", log_file = "", stream = True) HelpStr = '\nsudo python genexercise.py -a <IP Address or localhost> -c <path to genmon config file>\n' if os.geteuid() != 0: console.error("\nYou need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.\n") sys.exit(2) try: ConfigFilePath = ProgramDefaults.ConfPath address = ProgramDefaults.LocalHost opts, args = getopt.getopt(sys.argv[1:],"hc:a:",["help","configpath=","address="]) except getopt.GetoptError: console.error("Invalid command line argument.") sys.exit(2) for opt, arg in opts: if opt == '-h': console.error(HelpStr) sys.exit() elif opt in ("-a", "--address"): address = arg elif opt in ("-c", "--configpath"): ConfigFilePath = arg ConfigFilePath = ConfigFilePath.strip() port, loglocation = MySupport.GetGenmonInitInfo(ConfigFilePath, log = console) log = SetupLogger("client", loglocation + "genexercise.log") GenExerciseInstance = GenExercise(log = log, loglocation = loglocation, ConfigFilePath = ConfigFilePath, host = address, port = port) while True: time.sleep(0.5) sys.exit(1)