mirror of
https://github.com/jgyates/genmon.git
synced 2026-08-12 15:32:00 -04:00
4090 lines
173 KiB
Python
4090 lines
173 KiB
Python
#!/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 collections
|
|
import datetime
|
|
import json
|
|
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
import itertools
|
|
import struct
|
|
import copy
|
|
import re
|
|
|
|
from genmonlib.mylog import SetupLogger
|
|
from genmonlib.myplatform import MyPlatform
|
|
from genmonlib.mysupport import MySupport
|
|
from genmonlib.mythread import MyThread
|
|
from genmonlib.mytile import MyTile
|
|
from genmonlib.program_defaults import ProgramDefaults
|
|
|
|
# NOTE: collections OrderedDict is used for dicts that are displayed to the UI
|
|
|
|
|
|
# Fix Python 2.x. unicode type
|
|
if sys.version_info[0] >= 3: # PYTHON 3
|
|
unicode = str
|
|
|
|
|
|
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.Holding = collections.OrderedDict() # dict for registers and values (modbus fuction 03)
|
|
self.Strings = collections.OrderedDict() # dict for registers read a string data
|
|
self.FileData = collections.OrderedDict() # dict for modbus file reads (modbus function 0x14)
|
|
self.Coils = collections.OrderedDict() # dict for modbus coil reads (modbus fuction 01)
|
|
self.Inputs = collections.OrderedDict() # dict for modbus input registers (modbus function 4)
|
|
self.NotChanged = 0 # stats for registers
|
|
self.Changed = 0 # stats for registers
|
|
self.TotalChanged = 0.0 # ratio of changed ragisters
|
|
self.MaintLog = os.path.join(ConfigFilePath, "maintlog.json")
|
|
self.MaintLogList = []
|
|
self.MaintLock = threading.RLock()
|
|
self.OutageLog = os.path.join(ConfigFilePath, "outage.txt")
|
|
self.MinimumOutageDuration = 0
|
|
self.PowerLogMaxSize = 15.0 # 15 MB max size
|
|
self.PowerLog = os.path.join(ConfigFilePath, "kwlog.txt")
|
|
self.MaxPowerLogEntries = 8000
|
|
self.FuelLog = os.path.join(ConfigFilePath, "fuellog.txt")
|
|
self.FuelLock = threading.RLock()
|
|
self.PowerLogList = []
|
|
self.PowerLock = threading.RLock()
|
|
self.SensorLogPath = os.path.join(ConfigFilePath, "sensordata")
|
|
self.SensorLogMaxSize = 5.0 # 5 MB max size per sensor
|
|
self.MaxSensorLogEntries = 8000
|
|
self.SensorLogLock = threading.RLock()
|
|
self.SensorLogLists = {} # per-sensor cached lists: {sensor_name: [[ts, val], ...]}
|
|
self.bAlternateDateFormat = False
|
|
self.HoursFuelRemainingAtLoad = None
|
|
self.HoursFuelRemainingCurrentLoad = None
|
|
self.EstimatedFuleInTank = None
|
|
self.KWHoursMonth = None
|
|
self.FuelMonth = None
|
|
self.RunHoursMonth = None
|
|
self.TileList = [] # Tile list for GUI
|
|
self.TankData = None
|
|
self.FuelLevelOK = None # used in mynotify.py
|
|
self.debug = False
|
|
|
|
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.Phase = "Unknown"
|
|
self.NominalLineVolts = 240
|
|
self.EngineDisplacement = "Unknown"
|
|
self.TankSize = 0
|
|
self.UseExternalFuelData = False
|
|
self.UseExternalCTData = False
|
|
self.ExternalCTData = None
|
|
self.UseExternalSensorData = False
|
|
self.ExternalSensorData = None
|
|
self.ExternalSensorDataTime = None
|
|
self.ExternalSensorGagueData = None
|
|
self.ExternalDataLock = threading.RLock()
|
|
self.DisableOutageCheck = 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.OutageReoccuringNoticeTime = (self.ProgramStartTime)
|
|
self.OutageNoticeDelayTime = None
|
|
self.LastOutageDuration = self.OutageStartTime - self.OutageStartTime
|
|
self.OutageNoticeDelay = 0
|
|
self.Buttons = [] # UI command buttons (loaded after controller ID, if any)
|
|
|
|
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=ProgramDefaults.LogPath
|
|
)
|
|
self.UseMetric = self.config.ReadValue(
|
|
"metricweather", return_type=bool, default=False
|
|
)
|
|
self.debug = self.config.ReadValue(
|
|
"debug", return_type=bool, default=False
|
|
)
|
|
self.EnableDebug = self.config.ReadValue(
|
|
"enabledebug", return_type=bool, default=False
|
|
)
|
|
self.bDisplayExperimentalData = 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.FuelUnits = self.config.ReadValue("fuel_units", default="gal")
|
|
self.FuelHalfRate = self.config.ReadValue(
|
|
"half_rate", return_type=float, default=0.0
|
|
)
|
|
self.FuelFullRate = self.config.ReadValue(
|
|
"full_rate", return_type=float, default=0.0
|
|
)
|
|
self.UseExternalCTData = self.config.ReadValue(
|
|
"use_external_power_data", return_type=bool, default=False
|
|
)
|
|
# for gentankutil
|
|
self.UseExternalFuelData = self.config.ReadValue(
|
|
"use_external_fuel_data", return_type=bool, default=False
|
|
)
|
|
if not self.UseExternalFuelData:
|
|
# for gentankdiy
|
|
self.UseExternalFuelData = self.config.ReadValue(
|
|
"use_external_fuel_data_diy", return_type=bool, default=False
|
|
)
|
|
|
|
self.EstimateLoad = self.config.ReadValue(
|
|
"estimated_load", return_type=float, default=0.50
|
|
)
|
|
if self.EstimateLoad < 0:
|
|
self.EstimateLoad = 0
|
|
if self.EstimateLoad > 1:
|
|
self.EstimateLoad = 1
|
|
|
|
self.DisableOutageCheck = self.config.ReadValue(
|
|
"disableoutagecheck", return_type=bool, default=False
|
|
)
|
|
|
|
self.bDisableSensorLog = self.config.ReadValue(
|
|
"disablesensorlog", return_type=bool, default=False
|
|
)
|
|
|
|
self.SensorLogMaxSize = self.config.ReadValue(
|
|
"sensorlogmax", return_type=float, default=5.0
|
|
)
|
|
self.MaxSensorLogEntries = self.config.ReadValue(
|
|
"max_sensorlog_entries", return_type=int, default=8000
|
|
)
|
|
if self.config.HasOption("outagelog"):
|
|
self.OutageLog = self.config.ReadValue("outagelog")
|
|
self.LogError(
|
|
"Using alternate outage logfile: " + str(self.OutageLog)
|
|
)
|
|
|
|
if self.config.HasOption("kwlog"):
|
|
self.PowerLog = self.config.ReadValue("kwlog")
|
|
|
|
if self.config.HasOption("fuel_log"):
|
|
self.FuelLog = self.config.ReadValue("fuel_log")
|
|
self.FuelLog = self.FuelLog.strip()
|
|
|
|
self.UseFuelLog = self.config.ReadValue(
|
|
"enable_fuel_log", return_type=bool, default=False
|
|
)
|
|
self.FuelLogFrequency = self.config.ReadValue(
|
|
"fuel_log_freq", return_type=float, default=15.0
|
|
)
|
|
|
|
self.MinimumOutageDuration = self.config.ReadValue(
|
|
"min_outage_duration", return_type=int, default=0
|
|
)
|
|
self.PowerLogMaxSize = self.config.ReadValue(
|
|
"kwlogmax", return_type=float, default=15.0
|
|
)
|
|
self.MaxPowerLogEntries = self.config.ReadValue(
|
|
"max_powerlog_entries", return_type=int, default=8000
|
|
)
|
|
|
|
if self.config.HasOption("nominalfrequency"):
|
|
self.NominalFreq = self.config.ReadValue("nominalfrequency")
|
|
if not self.StringIsInt(self.NominalFreq):
|
|
self.NominalFreq = "Unknown"
|
|
if self.config.HasOption("nominalRPM"):
|
|
self.NominalRPM = self.config.ReadValue("nominalRPM")
|
|
if not self.StringIsInt(self.NominalRPM):
|
|
self.NominalRPM = "Unknown"
|
|
if self.config.HasOption("nominalKW"):
|
|
self.NominalKW = self.config.ReadValue("nominalKW")
|
|
if not self.StringIsFloat(self.NominalKW):
|
|
self.NominalKW = "Unknown"
|
|
if self.config.HasOption("model"):
|
|
self.Model = self.config.ReadValue("model")
|
|
|
|
self.NominalLineVolts = self.config.ReadValue("nominallinevolts", return_type=int, default=240)
|
|
|
|
self.Phase = self.config.ReadValue("phase", return_type=int, default=1)
|
|
|
|
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
|
|
)
|
|
|
|
self.OutageNoticeDelay = self.config.ReadValue(
|
|
"outage_notice_delay", return_type=int, default=0
|
|
)
|
|
|
|
self.bDisablePlatformStats = self.config.ReadValue(
|
|
"disableplatformstats", return_type=bool, default=False
|
|
)
|
|
self.bAlternateDateFormat = self.config.ReadValue(
|
|
"alternate_date_format", return_type=bool, default=False
|
|
)
|
|
|
|
self.ImportButtonFileList = []
|
|
self.ImportedButtons = []
|
|
ImportButtonsFiles = config.ReadValue("import_buttons",default=None)
|
|
|
|
if ImportButtonsFiles != None:
|
|
if len(ImportButtonsFiles):
|
|
ImportList = ImportButtonsFiles.strip().split(",")
|
|
if len(ImportList):
|
|
for Items in ImportList:
|
|
self.ImportButtonFileList.append(Items.strip())
|
|
|
|
# num minutes to send a warning email about an outage
|
|
self.OutageNoticeInterval = self.config.ReadValue("outage_notice_interval", return_type=int, default=0)
|
|
|
|
# the percentage of the total load of the allowable difference in current between legs
|
|
self.UnbalancedCapacity = self.config.ReadValue(
|
|
"unbalanced_capacity", return_type=float, default=0
|
|
)
|
|
if self.bDisablePlatformStats:
|
|
self.bUseRaspberryPiCpuTempGauge = False
|
|
self.bUseLinuxWifiSignalGauge = False
|
|
self.bWifiIsPercent = False
|
|
self.PreferredNetworkAdapter = None
|
|
else:
|
|
self.bUseRaspberryPiCpuTempGauge = self.config.ReadValue(
|
|
"useraspberrypicputempgauge", return_type=bool, default=True
|
|
)
|
|
self.bUseLinuxWifiSignalGauge = self.config.ReadValue(
|
|
"uselinuxwifisignalgauge", return_type=bool, default=True
|
|
)
|
|
self.bWifiIsPercent = self.config.ReadValue(
|
|
"wifiispercent", return_type=bool, default=False
|
|
)
|
|
self.PreferredNetworkAdapter = self.config.ReadValue(
|
|
"preferred_network_adapter", default=None
|
|
)
|
|
if self.PreferredNetworkAdapter != None:
|
|
self.PreferredNetworkAdapter = self.PreferredNetworkAdapter.strip()
|
|
except Exception as e1:
|
|
self.FatalError("Missing config file or config file entries: " + str(e1))
|
|
|
|
try:
|
|
if not self.bDisablePlatformStats:
|
|
self.Platform = MyPlatform(log=self.log, usemetric=self.UseMetric, net_adapter=self.PreferredNetworkAdapter, debug = self.debug)
|
|
if self.Platform.GetRaspberryPiTemp(ReturnFloat=True) == 0.0:
|
|
self.LogError("CPU Temp not supported.")
|
|
self.bUseRaspberryPiCpuTempGauge = False
|
|
# CPU temp is not supported on this platform
|
|
else:
|
|
self.Platform = None
|
|
except Exception as e1:
|
|
self.FatalError("Failure loading platform module: " + 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 = False)
|
|
self.Threads["CheckAlarmThread"].Start()
|
|
|
|
# start read thread to process incoming data commands
|
|
self.Threads["ProcessThread"] = MyThread(
|
|
self.ProcessThread, Name="ProcessThread", start = False)
|
|
self.Threads["ProcessThread"].Start()
|
|
|
|
if self.EnableDebug: # for debugging registers
|
|
self.LogDebug("STARTING REGISTER DEBUG THREAD")
|
|
self.Threads["DebugThread"] = MyThread(self.DebugThread, Name="DebugThread", start = False)
|
|
self.Threads["DebugThread"].Start()
|
|
|
|
# start thread for kw log
|
|
self.Threads["PowerMeter"] = MyThread(self.PowerMeter, Name="PowerMeter", start = False)
|
|
self.Threads["PowerMeter"].Start()
|
|
|
|
# start thread for sensor logging
|
|
self.Threads["SensorCollectionThread"] = MyThread(self.SensorCollectionThread, Name="SensorCollectionThread", start = False)
|
|
self.Threads["SensorCollectionThread"].Start()
|
|
|
|
self.Threads["MaintenanceHouseKeepingThread"] = MyThread(
|
|
self.MaintenanceHouseKeepingThread, Name="MaintenanceHouseKeepingThread", start = False)
|
|
self.Threads["MaintenanceHouseKeepingThread"].Start()
|
|
|
|
if self.UseFuelLog:
|
|
self.Threads["FuelLogger"] = MyThread(self.FuelLogger, Name="FuelLogger", start = False)
|
|
self.Threads["FuelLogger"].Start()
|
|
|
|
# ---------- GeneratorController:CheckForOutageCommon--------------------------
|
|
def CheckForOutageCommon(self, UtilityVolts, ThresholdVoltage, PickupVoltage):
|
|
|
|
try:
|
|
if (
|
|
UtilityVolts == None
|
|
or ThresholdVoltage == None
|
|
or PickupVoltage == None
|
|
):
|
|
return
|
|
|
|
if UtilityVolts < 0 or ThresholdVoltage < 0 or PickupVoltage < 0:
|
|
# invalid value
|
|
return
|
|
if UtilityVolts >= (self.NominalLineVolts * 2.5):
|
|
# ivalid value
|
|
return
|
|
|
|
# first time thru set the values to the same voltage level
|
|
if self.UtilityVoltsMin == 0 and self.UtilityVoltsMax == 0:
|
|
self.UtilityVoltsMin = UtilityVolts
|
|
self.UtilityVoltsMax = UtilityVolts
|
|
|
|
if UtilityVolts > self.UtilityVoltsMax:
|
|
if UtilityVolts > PickupVoltage:
|
|
self.UtilityVoltsMax = UtilityVolts
|
|
|
|
if UtilityVolts < self.UtilityVoltsMin:
|
|
if UtilityVolts > ThresholdVoltage:
|
|
self.UtilityVoltsMin = UtilityVolts
|
|
|
|
# Check for outage
|
|
# are we in an outage now
|
|
# NOTE: for now we are just comparing these numbers, the generator has a programmable delay
|
|
# that must be met once the voltage passes the threshold. This may cause some "switch bounce"
|
|
# testing needed
|
|
if self.SystemInOutage:
|
|
if UtilityVolts > PickupVoltage:
|
|
self.SystemInOutage = False
|
|
self.LastOutageDuration = (datetime.datetime.now() - self.OutageStartTime)
|
|
OutageStr = str(self.LastOutageDuration).split(".")[0] # remove microseconds from string
|
|
msgbody = ("\nUtility Power Restored at " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ". Duration of outage " + OutageStr)
|
|
self.MessagePipe.SendMessage("Outage Recovery Notice at " + self.SiteName,msgbody,msgtype="outage")
|
|
|
|
try:
|
|
if self.PowerMeterIsSupported() and self.FuelConsumptionSupported():
|
|
if (int(self.LastOutageDuration.total_seconds()) > 0):
|
|
# calling getpowerhistory with a duration of zero returns total fuel used so don't do that
|
|
FuelUsed = self.GetPowerHistory("power_log_json=%d,fuel" % int(self.LastOutageDuration.total_seconds()))
|
|
else:
|
|
# Outage of zero seconds...
|
|
if self.UseMetric:
|
|
FuelUsed = "0 L"
|
|
else:
|
|
FuelUsed = "0 gal"
|
|
if len(FuelUsed) and not "unknown" in FuelUsed.lower():
|
|
OutageStr += "," + FuelUsed
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error recording fuel usage for outage: " + str(e1))
|
|
# log outage to file
|
|
if (int(self.LastOutageDuration.total_seconds())> self.MinimumOutageDuration):
|
|
self.LogToFile(self.OutageLog, self.OutageStartTime.strftime("%Y-%m-%d %H:%M:%S"),OutageStr,)
|
|
else:
|
|
self.SendRecuringOutageNotice()
|
|
else:
|
|
if UtilityVolts < ThresholdVoltage:
|
|
if self.CheckOutageNoticeDelay():
|
|
self.SystemInOutage = True
|
|
self.OutageStartTime = datetime.datetime.now()
|
|
self.OutageReoccuringNoticeTime = datetime.datetime.now()
|
|
msgbody = ("\nUtility Power Out at "+ self.OutageStartTime.strftime("%Y-%m-%d %H:%M:%S"))
|
|
self.MessagePipe.SendMessage("Outage Notice at " + self.SiteName,msgbody,msgtype="outage",)
|
|
else:
|
|
self.OutageNoticeDelayTime = None
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in CheckForOutageCommon: " + str(e1))
|
|
return
|
|
|
|
# ------------ GeneratorController:SendRecuringOutageNotice ----------------
|
|
def SendRecuringOutageNotice(self):
|
|
try:
|
|
if not self.SystemInOutage:
|
|
return
|
|
if self.OutageNoticeInterval < 1:
|
|
return
|
|
|
|
LastOutageDuration = (datetime.datetime.now() - self.OutageStartTime)
|
|
if LastOutageDuration.total_seconds() <= self.MinimumOutageDuration:
|
|
return
|
|
|
|
if (datetime.datetime.now() - self.OutageReoccuringNoticeTime).total_seconds() / 60 < self.OutageNoticeInterval:
|
|
return
|
|
self.OutageReoccuringNoticeTime = datetime.datetime.now()
|
|
OutageStr = str(LastOutageDuration).split(".")[0] # remove microseconds from string
|
|
msgbody = ("\nUtility Outage Status: Untility power still out at " + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ". Duration of outage " + OutageStr)
|
|
self.MessagePipe.SendMessage("Recurring Outage Notice at " + self.SiteName,msgbody,msgtype="outage")
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SendRecuringOutageNotice: " + str(e1))
|
|
return
|
|
# ------------ GeneratorController:CheckOutageNoticeDelay ------------------
|
|
def CheckOutageNoticeDelay(self):
|
|
|
|
try:
|
|
if self.OutageNoticeDelay == 0:
|
|
return True
|
|
|
|
if self.OutageNoticeDelayTime == None:
|
|
self.OutageNoticeDelayTime = datetime.datetime.now()
|
|
return False
|
|
|
|
OutageNoticeDelta = datetime.datetime.now() - self.OutageNoticeDelayTime
|
|
if self.OutageNoticeDelay > OutageNoticeDelta.total_seconds():
|
|
return False
|
|
|
|
self.OutageNoticeDelayTime = None
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in CheckOutageNoticeDelay: " + str(e1))
|
|
return True
|
|
|
|
# ---------- 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()
|
|
else:
|
|
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(0.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:GeneratorIsRunning-------------------------
|
|
def GeneratorIsRunning(self):
|
|
|
|
return self.GetBaseStatus() in ["EXERCISING", "RUNNING", "RUNNING-MANUAL"]
|
|
|
|
# ---------- GeneratorController:FuelLogger---------------------------------
|
|
def FuelLogger(self):
|
|
|
|
if not self.UseFuelLog:
|
|
return
|
|
|
|
time.sleep(0.25)
|
|
while True:
|
|
if self.InitComplete:
|
|
break
|
|
if self.WaitForExit("FuelLogger", 1):
|
|
return
|
|
|
|
LastFuelValue = None
|
|
|
|
while True:
|
|
try:
|
|
if LastFuelValue != None and self.WaitForExit(
|
|
"FuelLogger", self.FuelLogFrequency * 60.0
|
|
):
|
|
return
|
|
|
|
if (
|
|
not self.ExternalFuelDataSupported()
|
|
and not self.FuelTankCalculationSupported()
|
|
and not self.FuelSensorSupported()
|
|
):
|
|
# this is an invalid setting so we do nothing, we do not exit to not flag a dead thread warning
|
|
LastFuelValue = 0.0
|
|
continue
|
|
|
|
FuelValue = self.GetFuelLevel(ReturnFloat=True)
|
|
|
|
if FuelValue == None:
|
|
if self.WaitForExit("FuelLogger", self.FuelLogFrequency * 60.0):
|
|
return
|
|
continue
|
|
if FuelValue == LastFuelValue:
|
|
continue
|
|
|
|
LastFuelValue = FuelValue
|
|
TimeStamp = datetime.datetime.now().strftime("%x %X")
|
|
with self.FuelLock:
|
|
self.LogToFile(self.FuelLog, TimeStamp, str(FuelValue))
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in FuelLogger: " + str(e1))
|
|
|
|
# ------------ GeneratorController::ClearFuelLog-----------------------------
|
|
def ClearFuelLog(self):
|
|
|
|
try:
|
|
if not len(self.FuelLog):
|
|
return "Fuel Not Present"
|
|
|
|
if not os.path.isfile(self.FuelLog):
|
|
return "Power Log is empty"
|
|
|
|
with self.FuelLock:
|
|
os.remove(self.FuelLog)
|
|
time.sleep(1)
|
|
|
|
return "Fuel Log cleared"
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ClearFuelLog: " + str(e1))
|
|
return "Error in ClearFuelLog: " + str(e1)
|
|
|
|
# ---------- GeneratorController:DebugThread--------------------------------
|
|
def DebugThread(self):
|
|
|
|
if not self.EnableDebug:
|
|
return
|
|
time.sleep(0.25)
|
|
|
|
MaxReg = 0x3000
|
|
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.ProcessTransaction(
|
|
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] = NewValue # update the value
|
|
|
|
msgbody = "\n"
|
|
try:
|
|
msgbody += json.dumps(RegistersUnderTest, indent=4, sort_keys=False)
|
|
except:
|
|
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:GetParameterStringValue-------------------
|
|
def GetParameterStringValue(
|
|
self, Register, ReturnString=False, offset=None, max=None
|
|
):
|
|
|
|
StringValue = self.Strings.get(Register, "")
|
|
if ReturnString:
|
|
if offset == None:
|
|
return self.HexStringToString(StringValue)
|
|
elif offset != None and max != None:
|
|
return self.HexStringToString(StringValue[offset:max])
|
|
elif offset != None and max == None:
|
|
return self.HexStringToString(StringValue[offset:])
|
|
elif offset == None and max != None:
|
|
return self.HexStringToString(StringValue[:max])
|
|
return StringValue
|
|
|
|
# -------------GeneratorController:GetParameterFileValue---------------------
|
|
def GetParameterFileValue(
|
|
self, Register, ReturnString=False, offset=None, max=None
|
|
):
|
|
|
|
StringValue = self.FileData.get(Register, "")
|
|
if ReturnString:
|
|
if offset == None:
|
|
return self.HexStringToString(StringValue)
|
|
elif offset != None and max != None:
|
|
return self.HexStringToString(StringValue[offset:max])
|
|
elif offset != None and max == None:
|
|
return self.HexStringToString(StringValue[offset:])
|
|
elif offset == None and max != None:
|
|
return self.HexStringToString(StringValue[:max])
|
|
return StringValue
|
|
|
|
# ------------ GeneratorController:GetRegisterValueFromList -----------------
|
|
def GetRegisterValueFromList(self, Register, IsCoil = False, IsInput = False):
|
|
|
|
try:
|
|
if IsCoil:
|
|
return self.Coils.get(Register, "")
|
|
if IsInput:
|
|
return self.Inputs.get(Register, "")
|
|
return self.Holding.get(Register, "")
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetRegisterValueFromList: " + str(e1))
|
|
return ""
|
|
|
|
# -------------GeneratorController:GetCoil-----------------------------------
|
|
def GetCoil(self, Register,OnLabel=None, OffLabel=None):
|
|
|
|
try:
|
|
if OnLabel != None and OffLabel != None:
|
|
DefaultReturn = False
|
|
else:
|
|
DefaultReturn = OffLabel
|
|
value = self.GetParameterBit(Register, 0x01, OnLabel = OnLabel, OffLabel = OffLabel, IsCoil = True)
|
|
if OnLabel != None and OffLabel != None:
|
|
return value
|
|
if value == 1:
|
|
return True
|
|
return False
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetCoil: " + str(e1))
|
|
return DefaultReturn
|
|
|
|
# -------------GeneratorController:GetParameterBit---------------------------
|
|
def GetParameterBit(self, Register, Mask, OnLabel=None, OffLabel=None, IsCoil = False, IsInput = False):
|
|
|
|
try:
|
|
Value = self.GetRegisterValueFromList(Register, IsCoil = IsCoil, IsInput = IsInput)
|
|
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,
|
|
IsCoil = False,
|
|
IsInput = 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, IsCoil = IsCoil, IsInput = IsInput)
|
|
ValueHi = self.GetParameter(RegisterHi, IsCoil = IsCoil, IsInput = IsInput)
|
|
|
|
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 round(FloatValue, 3)
|
|
return "%2.1f %s" % (FloatValue, LabelStr)
|
|
return "%d %s" % (IntValue, LabelStr)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetParameterLong: " + 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,
|
|
ReturnString=False,
|
|
IsCoil = False,
|
|
IsInput = False
|
|
):
|
|
|
|
try:
|
|
if ReturnInt:
|
|
DefaultReturn = 0
|
|
elif ReturnFloat:
|
|
DefaultReturn = 0.0
|
|
else:
|
|
DefaultReturn = ""
|
|
|
|
Value = self.GetRegisterValueFromList(Register, IsCoil = IsCoil, IsInput = IsInput)
|
|
if not len(Value):
|
|
return DefaultReturn
|
|
|
|
if ReturnString == True:
|
|
return self.HexStringToString(Value)
|
|
|
|
if Divider == None and Label == None:
|
|
if Hex:
|
|
return Value
|
|
elif ReturnFloat:
|
|
return float(int(Value, 16))
|
|
elif ReturnInt:
|
|
return int(Value, 16)
|
|
else:
|
|
return str(int(Value, 16))
|
|
|
|
IntValue = int(Value, 16)
|
|
if not Divider == None:
|
|
FloatValue = IntValue / Divider
|
|
if ReturnInt:
|
|
return int(FloatValue)
|
|
if ReturnFloat:
|
|
return round(FloatValue, 3)
|
|
if not Label == None:
|
|
return "%.2f %s" % (FloatValue, Label)
|
|
else:
|
|
return "%.2f" % (FloatValue)
|
|
elif ReturnInt:
|
|
return IntValue
|
|
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
|
|
|
|
# -------------CustomController:SetCommandButton-----------------------------
|
|
def SetCommandButton(self, CommandString):
|
|
try:
|
|
ValidInput = False
|
|
EntryString = CommandString
|
|
if EntryString == None or not len(EntryString):
|
|
return "Error: Invalid input for Set Button Command entry."
|
|
|
|
EntryString = EntryString.strip()
|
|
if EntryString.startswith("set_button_command"):
|
|
EntryString = EntryString[len("set_button_command") :]
|
|
EntryString = EntryString.strip()
|
|
if EntryString.strip().startswith("="):
|
|
EntryString = EntryString[len("=") :]
|
|
EntryString = EntryString.strip()
|
|
ValidInput = True
|
|
|
|
if ValidInput:
|
|
try:
|
|
CommandSetList = json.loads(EntryString)
|
|
# validate object
|
|
if not isinstance(CommandSetList, list) and not (len(CommandSetList) == 0):
|
|
self.LogError("Invalid button object in SetCommandButton")
|
|
return "Error: Invalid button object"
|
|
# Execute Command
|
|
return self.ExecuteRemoteCommand(CommandSetList)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetCommandButton: " + str(e1))
|
|
return "Error: Invalid input for SetCommandButton (2), see error log."
|
|
else:
|
|
self.LogError("Error in SetCommandButton: invalid input: " + str(CommandString))
|
|
return "Error: Invalid input for SetCommandButton (3)."
|
|
return "OK"
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetCommandButton: " + str(e1))
|
|
return "Error in SetCommandButton, see error log."
|
|
return "OK"
|
|
|
|
# -------------Controller:GetButtons---------------------------------------
|
|
def GetButtons(self, singlebuttonname = None):
|
|
try:
|
|
|
|
if len(self.Buttons) < 1:
|
|
return []
|
|
button_list = self.Buttons
|
|
button_list = self.GetButtonsCommon(button_list, singlebuttonname=singlebuttonname)
|
|
return button_list
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetButtons: " + str(e1))
|
|
return []
|
|
# ---------- Controller::LoadButtonsFromFile-------------------------------
|
|
def LoadButtonsFromFile(self):
|
|
try:
|
|
if self.ImportButtonFileList == None or len(self.ImportButtonFileList) == 0:
|
|
return []
|
|
ImportedButtons = []
|
|
|
|
for FileName in self.ImportButtonFileList:
|
|
ConfigFileName = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
|
|
"data",
|
|
"commands",
|
|
FileName
|
|
)
|
|
if os.path.isfile(ConfigFileName):
|
|
try:
|
|
with open(ConfigFileName) as infile:
|
|
command_import = json.load(infile)
|
|
ImportedButtons.extend(command_import["buttons"])
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in LoadButtonsFromFile reading config import file: " + str(e1))
|
|
continue
|
|
else:
|
|
self.LogError("Error in LoadButtonsFromFile reading button import file: " + str(ConfigFileName))
|
|
continue
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in LoadButtonsFromFile: " + str(e1))
|
|
|
|
return ImportedButtons
|
|
|
|
# ---------- Controller::GetButtonsCommon----------------------------------
|
|
def GetButtonsCommon(self, button_list, singlebuttonname = None):
|
|
try:
|
|
if len(self.ImportButtonFileList) == 0 and button_list == None:
|
|
return []
|
|
|
|
if not len(self.ImportedButtons):
|
|
self.ImportedButtons = self.LoadButtonsFromFile()
|
|
|
|
# combine lists only on first (and only) import
|
|
if button_list == None or len(button_list) == 0:
|
|
button_list = self.ImportedButtons
|
|
else:
|
|
button_list.extend(self.ImportedButtons)
|
|
|
|
if not isinstance(button_list, list):
|
|
self.LogError("Error in GetButtonsCommon: invalid input or data: "+ str(type(button_list)))
|
|
return []
|
|
|
|
# Validate buttons before sending to the web app
|
|
return_buttons = []
|
|
for button in button_list:
|
|
|
|
if not "onewordcommand" in button.keys():
|
|
self.LogError("Error in GetButtonsCommon: button must have onewordcommand element: "+ str(button))
|
|
continue
|
|
elif not isinstance(button["onewordcommand"], str):
|
|
self.LogError("Error in GetButtonsCommon: invalid button defined validateing onewordcommand (non string): "+ str(button))
|
|
continue
|
|
if not "title" in button.keys():
|
|
self.LogError("Error in GetButtonsCommon: button must have title element: "+ str(button))
|
|
continue
|
|
elif not isinstance(button["title"], str):
|
|
self.LogError("Error in GetButtonsCommon: invalid button defined validateing title (not string): "+ str(button))
|
|
continue
|
|
if not "command_sequence" in button.keys():
|
|
self.LogError("Error in GetButtonsCommon: button must have command_sequence element: "+ str(button))
|
|
continue
|
|
elif not isinstance(button["command_sequence"], list):
|
|
self.LogError("Error in GetButtonsCommon: invalid button defined validateing command_sequence:(not list) "+ str(button))
|
|
continue
|
|
|
|
# valiate command sequeuence
|
|
CommandError = False
|
|
for command in button["command_sequence"]:
|
|
if not "reg" in command.keys() or not isinstance(command["reg"], str):
|
|
self.LogError("Error in GetButtonsCommon: invalid command string defined validateing reg: "+ str(button))
|
|
CommandError = True
|
|
break
|
|
if "reg_type" in command.keys():
|
|
if not command["reg_type"].lower() in ["holding","coil","script","singlecoil","singleholding"]:
|
|
self.LogError("Error in GetButtonsCommon: Error validateing re_type: "+ str(button))
|
|
CommandError = True
|
|
break
|
|
|
|
if not "value" in command.keys():
|
|
if "reg_type" in command.keys() and command["reg_type"].lower() == "script":
|
|
# other fields are not required, "reg" has script name
|
|
continue
|
|
# this command requires input from the web app, let's validate the params
|
|
# "input_title", "type" is required. "length" is default 2 but must be a multiple of 2
|
|
if not "input_title" in command.keys() or not "type" in command.keys():
|
|
self.LogError("Error in GetButtonsCommon: Error validateing input_title and type: "+ str(button))
|
|
CommandError = True
|
|
break
|
|
if "length" in command.keys():
|
|
if(int(command["length"]) % 2 != 0):
|
|
self.LogError("Error in GetButtonsCommon: length of command_sequence input must be a multiple of 2: " + str(button))
|
|
CommandError = True
|
|
break
|
|
if "bounds_regex" in command.keys():
|
|
if not self.RegExIsValid(command["bounds_regex"]):
|
|
self.LogError("Error in GetButtonsCommon: invalid regular expression for bounds_regex in command_sequence: " + str(button))
|
|
CommandError = True
|
|
break
|
|
if CommandError:
|
|
continue
|
|
|
|
if singlebuttonname != None and singlebuttonname == button["onewordcommand"]:
|
|
return button
|
|
|
|
return_buttons.append(button)
|
|
|
|
return return_buttons
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetButtonsCommon: " + str(e1))
|
|
self.ImportedButtons = []
|
|
return []
|
|
|
|
# -------------CustomController:ExecuteRemoteCommand-------------------------
|
|
def ExecuteRemoteCommand(self, CommandSetList):
|
|
# CommandSetList is a list of dicts, each dict is a command to execute
|
|
# Each dict in the list is in the format of the objects in the 'buttons'
|
|
# list in the custom controller JSON format
|
|
try:
|
|
if sys.version_info[0] < 3: #
|
|
self.LogError("Error in ExecuteRemoteCommand, requires python3: " + str(sys.version_info.major) + "." + str(sys.version_info.minor))
|
|
return "Error in ExecuteRemoteCommand, requires python3"
|
|
if not isinstance(CommandSetList, list):
|
|
return "Error: invalid input in ExecuteRemoteCommand"
|
|
|
|
with self.ModBus.CommAccessLock:
|
|
# put the lock here so if there are multiple commands they will be executed back to back
|
|
for button_command in CommandSetList:
|
|
if not isinstance(button_command, dict) and not len(button_command) == 1:
|
|
self.LogError("Error on ExecuteRemoteCommand, expecting single dict: " + str(button_command))
|
|
return "Error: invalid input in ExecuteRemoteCommand"
|
|
if not "onewordcommand" in button_command.keys():
|
|
self.LogError("Error on ExecuteRemoteCommand, invalid dict: " + str(button_command))
|
|
return "Error: invalid input in ExecuteRemoteCommand (2)"
|
|
# make a copy of the dict so we can add the input without modifying the original
|
|
returndict = self.GetButtons(singlebuttonname = button_command["onewordcommand"])
|
|
if returndict == None:
|
|
self.LogError("Error on ExecuteRemoteCommand, command not found: " + str(button_command))
|
|
return "Error: invalid input in ExecuteRemoteCommand command not found"
|
|
selected_command = copy.deepcopy(returndict)
|
|
if not len(selected_command):
|
|
self.LogError("Error on ExecuteRemoteCommand, invalid command: " + str(button_command))
|
|
return "Error: invalid command in ExecuteRemoteCommand (2)"
|
|
|
|
# selected_command from genmon, button_command from UI
|
|
if not "command_sequence" in selected_command.keys() or not "command_sequence" in button_command.keys():
|
|
self.LogError("Error on ExecuteRemoteCommand, command sequence mismatch: " + str(button_command))
|
|
return "Error on ExecuteRemoteCommand, command sequence mismatch"
|
|
if not (len(selected_command["command_sequence"]) == len(button_command["command_sequence"])):
|
|
self.LogError("Error on ExecuteRemoteCommand, command sequence mismatch (2): " + str(button_command))
|
|
return "Error on ExecuteRemoteCommand, command sequence mismatch (2)"
|
|
# iterate thru both lists of commands
|
|
for gm_cmd, ui_cmd in zip(selected_command["command_sequence"], button_command["command_sequence"]):
|
|
if "input_title" in gm_cmd.keys() and "value" in ui_cmd.keys():
|
|
if "bounds_regex" in gm_cmd.keys():
|
|
if not re.match(gm_cmd["bounds_regex"], str(ui_cmd["value"])):
|
|
self.LogError("Error in ExecuteRemoteCommand: Failed bounds check: " + str(ui_cmd))
|
|
return "Error in ExecuteRemoteCommand: Failed bounds check"
|
|
if "type" in gm_cmd.keys() and gm_cmd["type"] == "int":
|
|
if not "length" in gm_cmd.keys() or ("length" in gm_cmd.keys() and gm_cmd["length"] == 2):
|
|
gm_cmd["value"] = "%04x" % int(ui_cmd["value"])
|
|
elif "length" in gm_cmd and gm_cmd["length"] == 4:
|
|
gm_cmd["value"] = "%08x" % int(ui_cmd["value"])
|
|
else:
|
|
self.LogError("Error in ExecuteRemoteCommand: only 2 or 4 supported for int lenght: " + str(gm_cmd))
|
|
return "Error in ExecuteRemoteCommand, invalid length of input"
|
|
else:
|
|
self.LogError("Error in ExecuteRemoteCommand, unsupported type: " + str(ui_cmd))
|
|
return "Error in ExecuteRemoteCommand, unsupported type"
|
|
elif not "reg" in gm_cmd.keys() or not "value" in gm_cmd.keys():
|
|
self.LogError("Error in ExecuteRemoteCommand, invalid command in sequence: " + str(selected_command))
|
|
self.LogDebug(str(button_command))
|
|
return "Error in ExecuteRemoteCommand, invalid command in sequence"
|
|
# execute the command selected_command
|
|
return self.ExecuteCommandSequence(selected_command["command_sequence"])
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ExecuteRemoteCommand: " + str(e1))
|
|
self.LogDebug(str(CommandSetList))
|
|
return "Error in ExecuteRemoteCommand"
|
|
return "OK"
|
|
# -------------CustomController:ExecuteCommandSequence-----------------------
|
|
def ExecuteCommandSequence(self, command_sequence):
|
|
try:
|
|
with self.ModBus.CommAccessLock:
|
|
for command in command_sequence:
|
|
IsCoil = False # can only be holding or coil
|
|
IsSingle = False
|
|
if not "reg" in command.keys():
|
|
self.LogDebug("Error in ExecuteCommandSequence: invalid value array, no 'reg' in command_sequence command: " + str(command))
|
|
continue
|
|
if "reg_type" in command.keys() and command["reg_type"] == "script":
|
|
# if we get here then we execute a script with the filename of the "reg" entry
|
|
ScriptFileName = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
|
|
"data",
|
|
"commands",
|
|
"script",
|
|
command["reg"]
|
|
)
|
|
|
|
try:
|
|
import subprocess
|
|
|
|
OutputStream = subprocess.PIPE
|
|
executelist = [sys.executable, ScriptFileName]
|
|
pid = subprocess.Popen(
|
|
executelist,
|
|
stdout=OutputStream,
|
|
stderr=OutputStream,
|
|
stdin=OutputStream,
|
|
)
|
|
#subprocess.call(ScriptFileName, shell=True)
|
|
self.LogDebug("Script Name: " + ScriptFileName)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error calling script for button: " + ScriptFileName + " : " + str(e1))
|
|
continue
|
|
if not isinstance(command["value"],int) and not len(command["value"]):
|
|
self.LogDebug("Error in ExecuteCommandSequence: invalid value array")
|
|
continue
|
|
if "reg_type" in command.keys() and command["reg_type"] == "coil":
|
|
IsCoil = True
|
|
elif "reg_type" in command.keys() and command["reg_type"] == "singlecoil":
|
|
IsCoil = True
|
|
IsSingle = True
|
|
elif "reg_type" in command.keys() and command["reg_type"] == "singleholding":
|
|
IsSingle = True
|
|
|
|
if "format" in command.keys() and len(command["format"]):
|
|
if command["type"] == "int":
|
|
ReturnValue = command["format"] % int(command["value"],16)
|
|
command["value"] = ReturnValue
|
|
|
|
if "length" in command.keys() and command["length"] == (len(command["value"]) / 2) and isinstance(command["value"], str):
|
|
# This assumes a multi word write so convert it to a list of hex values
|
|
command["value"] = list(bytes.fromhex(command["value"]))
|
|
|
|
|
|
if isinstance(command["value"], list):
|
|
if not (len(command["value"]) % 2) == 0:
|
|
self.LogDebug("Error in ExecuteCommandSequence: invalid value length")
|
|
return "Command not found."
|
|
Data = []
|
|
for item in command["value"]:
|
|
if isinstance(item, str):
|
|
item = int(item, 16)
|
|
item = self.ProcessBitModifiers(command, item)
|
|
Data.append(item)
|
|
elif isinstance(item, int):
|
|
item = self.ProcessBitModifiers(command, item)
|
|
Data.append(item)
|
|
else:
|
|
self.LogDebug("Error in ExecuteCommandSequence: invalid type if value list")
|
|
return "Command not found."
|
|
self.LogDebug("Write List: len: " + str(int(len(Data) // 2)) + " : " + self.LogHexList(Data, prefix=command["reg"], nolog = True))
|
|
self.ModBus.ProcessWriteTransaction(command["reg"], len(Data) // 2, Data, IsCoil = IsCoil, IsSingle = IsSingle)
|
|
self.DelayBetweenFrames(ignoreerror = True)
|
|
elif isinstance(command["value"], str):
|
|
# only supports single word writes
|
|
value = int(command["value"], 16)
|
|
value = self.ProcessBitModifiers(command, value)
|
|
LowByte = value & 0x00FF
|
|
HighByte = (value >> 8) & 0x00ff
|
|
Data = []
|
|
Data.append(HighByte)
|
|
Data.append(LowByte)
|
|
self.LogDebug("Write Str: len: "+ str(int(len(Data) // 2)) + " : " + command["reg"] + ": "+ ("%04x %04x" % (HighByte, LowByte)))
|
|
self.ModBus.ProcessWriteTransaction(command["reg"], len(Data) // 2, Data, IsCoil = IsCoil, IsSingle = IsSingle)
|
|
self.DelayBetweenFrames(ignoreerror = True)
|
|
elif isinstance(command["value"], int):
|
|
# only supports single word writes
|
|
value = command["value"]
|
|
value = self.ProcessBitModifiers(command, value)
|
|
LowByte = value & 0x00FF
|
|
HighByte = (value >> 8) & 0x00ff
|
|
Data = []
|
|
Data.append(HighByte)
|
|
Data.append(LowByte)
|
|
self.LogDebug("Write Int: len: "+ str(int(len(Data) // 2)) + " : " + command["reg"]+ ": "+ ("%04x %04x" % (HighByte, LowByte)))
|
|
self.ModBus.ProcessWriteTransaction(command["reg"], len(Data) // 2, Data, IsCoil = IsCoil, IsSingle = IsSingle)
|
|
self.DelayBetweenFrames(ignoreerror = True)
|
|
else:
|
|
self.LogDebug("Error in ExecuteCommandSequence: invalid value type")
|
|
return "Command not found."
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ExecuteCommandSequence: " + str(e1))
|
|
self.LogDebug(str(command_sequence))
|
|
return "Error in ExecuteCommandSequence"
|
|
return "The command was sent to the controller."
|
|
|
|
# ------------ GeneratorController:ProcessBitModifiers ----------------------
|
|
def ProcessBitModifiers(self, entry, value, ReturnFloat = False):
|
|
try:
|
|
orignialvalue = value
|
|
|
|
if "swapwords32" in entry.keys():
|
|
# change from 01234567 to 456701234
|
|
if entry["swapwords32"] == True:
|
|
value = self.SwapWords32(value)
|
|
if "shiftright" in entry.keys():
|
|
value = value >> int(entry["shiftright"])
|
|
if "shiftleft" in entry.keys():
|
|
value = value << int(entry["shiftleft"])
|
|
if "ieee754" in entry.keys() and ReturnFloat:
|
|
# Use this for testing: https://www.h-schmidt.net/FloatConverter/IEEE754.html
|
|
if entry["ieee754"].lower() == "half": # 16 bits
|
|
value = float(struct.unpack('f', struct.pack('H', int(value)))[0])
|
|
elif entry["ieee754"].lower() == "single": # 32 bits
|
|
value = float(struct.unpack('f', struct.pack('I', int(value)))[0])
|
|
elif entry["ieee754"].lower() == "double": # 64 bits
|
|
value = float(struct.unpack('f', struct.pack('Q', int(value)))[0])
|
|
else:
|
|
self.LogError("Error converting IEEE754 floating point: invalid ieee754 type: " + str(entry))
|
|
if math.isnan(value):
|
|
self.LogDebug("Error: value is not an IEEE floating point number: " + ("%x" % orignialvalue) + " : " + str(entry["title"]) + ": " + str(value))
|
|
return 0.0
|
|
if "multiplier" in entry.keys():
|
|
if ReturnFloat:
|
|
value = float(value * float(entry["multiplier"]))
|
|
else:
|
|
value = int(value * float(entry["multiplier"]))
|
|
return value
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ProcessBitModifiers: " + str(e1) + ": " + str(entry["title"]))
|
|
return value
|
|
|
|
# --------------------GeneratorController:DelayBetweenFrames----------------
|
|
def DelayBetweenFrames(self, ignoreerror = False):
|
|
# add a delay between modbus requests
|
|
try:
|
|
if self.ModBus.BetweenFrameDelay <= 0:
|
|
return
|
|
|
|
self.WaitForExit(timeout = self.ModBus.BetweenFrameDelay, ignoreerror=ignoreerror)
|
|
except Exception as e1:
|
|
self.LogErrorLine(f"Error in DelayBetween Frames: {e1}")
|
|
|
|
# ------------ 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["phase"] = self.Phase
|
|
StartInfo["Controller"] = "Generic Controller Name"
|
|
StartInfo["PowerGraph"] = self.PowerMeterIsSupported()
|
|
StartInfo["Sensors"] = self.GetSensorNames()
|
|
StartInfo["NominalBatteryVolts"] = "12"
|
|
StartInfo["UtilityVoltageDisplayed"] = True
|
|
StartInfo["RemoteCommands"] = True
|
|
StartInfo["RemoteButtons"] = False
|
|
StartInfo["Linux"] = self.Platform.IsOSLinux()
|
|
StartInfo["RaspberryPi"] = self.Platform.IsPlatformRaspberryPi()
|
|
StartInfo["Platform"] = self.Platform.PlatformName()
|
|
StartInfo["import_config_file"] = None
|
|
|
|
if not NoTile:
|
|
|
|
StartInfo["buttons"] = self.GetButtons()
|
|
|
|
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:GetMessageText ------------------------------------
|
|
def GetMessageText(self):
|
|
try:
|
|
msgtext = self.DisplayStatus()
|
|
msgtext += self.DisplayMaintenance()
|
|
return msgtext
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetMessageText: " + str(e1))
|
|
return ""
|
|
|
|
# ---------- 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::SetGeneratorRemoteCommand---------------
|
|
# 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 SetGeneratorRemoteCommand(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 few 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 self.GetSwitchState() + " : " + self.GetEngineState()
|
|
|
|
# ------------ 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.ProcessTransaction(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:WriteRegValue ---------------------------
|
|
def WriteRegValue(self, CmdString):
|
|
|
|
# extract quiet mode setting from Command String
|
|
# Format we are looking for is "writeregvalue=01f4,aa"
|
|
msgbody = "Invalid command syntax for command writeregvalue"
|
|
try:
|
|
|
|
CmdList = CmdString.split("=")
|
|
if len(CmdList) != 2:
|
|
self.LogError("Validation Error: Error parsing command string in WriteRegValue (parse): " + CmdString)
|
|
return msgbody
|
|
|
|
CmdList[0] = CmdList[0].strip()
|
|
|
|
if not CmdList[0].lower() == "writeregvalue":
|
|
self.LogError("Validation Error: Error parsing command string in WriteRegValue (parse2): " + CmdString)
|
|
return msgbody
|
|
|
|
ParsedList = CmdList[1].split(",")
|
|
|
|
if len(ParsedList) != 2:
|
|
self.LogError("Validation Error: Error parsing command string in WriteRegValue (parse3): " + CmdString)
|
|
return msgbody
|
|
Register = ParsedList[0].strip()
|
|
Value = ParsedList[1].strip()
|
|
Value = int(Value,16)
|
|
LowByte = Value & 0x00FF
|
|
HighByte = Value >> 8
|
|
Data = []
|
|
Data.append(HighByte)
|
|
Data.append(LowByte)
|
|
RegValue = self.ModBus.ProcessWriteTransaction(Register, len(Data) // 2, Data)
|
|
self.DelayBetweenFrames(ignoreerror = True)
|
|
|
|
if RegValue == "":
|
|
msgbody = "OK"
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Validation Error: Error parsing command string in WriteRegValue: " + CmdString)
|
|
self.LogError(str(e1))
|
|
return msgbody
|
|
|
|
return msgbody
|
|
|
|
# ------------ GeneratorController:DisplayOutageHistory----------------------
|
|
def DisplayOutageHistory(self, JSONNum=False):
|
|
|
|
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
|
|
line = self.removeNonPrintable(line)
|
|
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()
|
|
|
|
index = 0
|
|
for Items in OutageLog:
|
|
if len(Items) > 1:
|
|
try:
|
|
# should be format yyyy-mm-dd hh:mm:ss
|
|
EntryDate = datetime.datetime.strptime(Items[0], "%Y-%m-%d %H:%M:%S")
|
|
if self.bAlternateDateFormat:
|
|
FormattedDate = EntryDate.strftime("%d-%m-%Y %H:%M:%S")
|
|
else:
|
|
FormattedDate = EntryDate.strftime("%m-%d-%Y %H:%M:%S")
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error parsing date/time in outage log: " + str(e1))
|
|
continue
|
|
if len(Items) == 2:
|
|
if JSONNum:
|
|
LogHistory.append({index: [{"Date": FormattedDate}, {"Duration": Items[1]}]})
|
|
else:
|
|
LogHistory.append("%s, Duration: %s" % (FormattedDate, Items[1]))
|
|
elif len(Items) == 3:
|
|
if JSONNum:
|
|
LogHistory.append({index:[{"Date": FormattedDate}, {"Duration": Items[1]},{"Estimated Fuel": Items[2]}]})
|
|
else:
|
|
LogHistory.append("%s, Duration: %s, Estimated Fuel: %s"% (FormattedDate, Items[1], Items[2]))
|
|
index += 1
|
|
|
|
return LogHistory
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayOutageHistory: " + str(e1))
|
|
return []
|
|
|
|
# ------------ GeneratorController::LogToPowerLog----------------------------
|
|
def LogToPowerLog(self, TimeStamp, Value):
|
|
|
|
try:
|
|
TimeStamp = self.removeNonPrintable(TimeStamp)
|
|
Value = self.removeNonPrintable(Value)
|
|
if not len(TimeStamp) or not len(Value):
|
|
self.LogError(
|
|
"Invalid entry in LogToPowerLog: "
|
|
+ str(TimeStamp)
|
|
+ ","
|
|
+ str(Value)
|
|
)
|
|
return
|
|
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:
|
|
# if we have too many entries, then delete some
|
|
if len(PowerList) > MaxSize:
|
|
return self.RemovePowerSamples(PowerList, MaxSize)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in RecducePowerSamples: %s" % str(e1))
|
|
return PowerList
|
|
|
|
return PowerList
|
|
|
|
# ------------ GeneratorController::AverageTwoSamples-----------------------
|
|
def AverageTwoSamples(self, a, b):
|
|
try:
|
|
if float(a[1]) == 0:
|
|
return a
|
|
if float(b[1]) == 0:
|
|
return b
|
|
|
|
average = (float(a[1]) + float(b[1])) / 2.0
|
|
average = round(average, 2)
|
|
return [a[0], str(average)]
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in AverageTwoSamples: %s" % str(e1))
|
|
return a
|
|
# ------------ GeneratorController::ReduceList------------------------------
|
|
def ReduceList(self, inputlist):
|
|
try:
|
|
|
|
if len(inputlist) < 2:
|
|
return inputlist
|
|
if len(inputlist) % 2 == 0:
|
|
# even
|
|
Lenght = len(inputlist)
|
|
isEven = True
|
|
else:
|
|
Lenght = len(inputlist) - 1
|
|
isEven = False
|
|
OutList = []
|
|
|
|
for k in range(0,Lenght,2):
|
|
try:
|
|
# check to see if there are zero entries for the power
|
|
if float(inputlist[k][1]) == 0 or float(inputlist[k+1][1]) == 0:
|
|
OutList.append(inputlist[k])
|
|
OutList.append(inputlist[k+1])
|
|
else:
|
|
OutList.append(self.AverageTwoSamples(inputlist[k],inputlist[k+1]))
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ReduceList (2): " + str(e1) +", " + str(k) + ", " + str(Lenght))
|
|
self.LogErrorLine("isEven = " + str(isEven))
|
|
if not isEven:
|
|
OutList.append(inputlist[-1])
|
|
return OutList
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ReduceList: %s" % str(e1))
|
|
return inputlist
|
|
# ------------ GeneratorController::RemovePowerSamples-----------------------
|
|
def RemovePowerSamples(self, inputList, MaxSize):
|
|
|
|
try:
|
|
|
|
if len(inputList) <= MaxSize:
|
|
self.LogError("RemovePowerSamples: Error: Can't remove ")
|
|
return inputList
|
|
|
|
|
|
'''
|
|
This code is removed as it is a work in progress to reduce the working set of power log samples
|
|
NewList = inputList[:] # copy
|
|
try:
|
|
for i in range(10):
|
|
if len(NewList) <= MaxSize:
|
|
break
|
|
NewList = self.ReduceList(NewList)
|
|
|
|
except Exception as e1:
|
|
self.LogError("RemovePowerSamples: Error: in list reduction: " + str(e1))
|
|
|
|
self.LogDebug("Post Reduce RemovePowerSamples:" + str(len(NewList)))'
|
|
'''
|
|
# This will just remove all samples but the first MaxSize. This will only do anything if the above
|
|
# code failes to find valid samples to remove (i.e. all samples are zero)
|
|
if len(inputList) > MaxSize:
|
|
NewList = inputList[:MaxSize]
|
|
|
|
return NewList
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in RemovePowerSamples: %s" % str(e1))
|
|
return NewList
|
|
|
|
# ------------ GeneratorController::GetPowerLogForMinutes--------------------
|
|
def GetPowerLogForMinutes(self, Minutes=0, InputList = None):
|
|
try:
|
|
ReturnList = []
|
|
if InputList == None:
|
|
PowerList = self.ReadPowerLogFromFile()
|
|
else:
|
|
PowerList = InputList
|
|
if not Minutes:
|
|
return PowerList
|
|
CurrentTime = datetime.datetime.now()
|
|
|
|
# PowerList is newest-first (ReadPowerLogFromFile inserts each appended
|
|
# power-log line at index 0). Iterate newest-first and stop at the first
|
|
# entry older than the requested window -- every later entry is older
|
|
# too -- instead of strptime-parsing the entire power log (up to
|
|
# MaxPowerLogEntries) on every call. Output is unchanged.
|
|
for Time, Power in PowerList:
|
|
try:
|
|
struct_time = time.strptime(Time, "%x %X")
|
|
LogEntryTime = datetime.datetime.fromtimestamp(time.mktime(struct_time))
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetPowerLogForMinutes: " + str(e1))
|
|
continue
|
|
Delta = CurrentTime - LogEntryTime
|
|
if self.GetDeltaTimeMinutes(Delta) < Minutes:
|
|
ReturnList.append([Time, Power])
|
|
else:
|
|
break
|
|
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 = []
|
|
|
|
with self.PowerLock:
|
|
# return cached list if we have read the file before
|
|
if len(self.PowerLogList) and not Minutes:
|
|
return self.PowerLogList
|
|
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
|
|
line = self.removeNonPrintable(line)
|
|
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) > self.MaxPowerLogEntries and not NoReduce:
|
|
PowerList = self.ReducePowerSamples(PowerList, self.MaxPowerLogEntries)
|
|
if not len(self.PowerLogList):
|
|
self.PowerLogList = PowerList
|
|
return PowerList
|
|
|
|
# ------------ GeneratorController::GetPowerHistory--------------------------
|
|
def GetPowerHistory(self, CmdString, NoReduce=False, FromUI=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:
|
|
if FromUI and Minutes == 0 and KWHours == False and FuelConsumption == False and RunHours == False:
|
|
# if raw log is requested and minutes are zero and from the UI then reduce to 31 days
|
|
self.LogDebug("Reducing from UI: " + CmdString)
|
|
Minutes = (60 *24 * 31) # Minutes in month
|
|
|
|
PowerList = self.ReadPowerLogFromFile(Minutes=Minutes)
|
|
|
|
# Shorten list to self.MaxPowerLogEntries if specific duration requested
|
|
# if not KWHours and len(PowerList) > self.MaxPowerLogEntries and Minutes and not NoReduce:
|
|
if len(PowerList) > self.MaxPowerLogEntries and Minutes and not NoReduce:
|
|
PowerList = self.ReducePowerSamples(PowerList, self.MaxPowerLogEntries)
|
|
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"
|
|
if Consumption < 0:
|
|
self.LogDebug("WARNING: Fuel Consumption is less than zero in GetPowerHistory: %d" % Consumption)
|
|
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:
|
|
try:
|
|
# is the power value a float?
|
|
Power = float(Items[1])
|
|
except Exception as e1:
|
|
Power = 0.0
|
|
continue
|
|
try:
|
|
# This should be date time
|
|
struct_time = time.strptime(Items[0], "%x %X")
|
|
LogEntryTime = datetime.datetime.fromtimestamp(
|
|
time.mktime(struct_time)
|
|
)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Invalid time entry in power log: " + str(e1))
|
|
continue
|
|
|
|
# Changes in Daylight savings time will effect this
|
|
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::DecimateList--------------------------------
|
|
def DecimateList(self, inputList, MaxSize):
|
|
try:
|
|
if len(inputList) <= MaxSize:
|
|
return inputList
|
|
step = float(len(inputList) - 1) / float(MaxSize - 1)
|
|
result = []
|
|
for i in range(MaxSize):
|
|
idx = int(round(i * step))
|
|
result.append(inputList[idx])
|
|
return result
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DecimateList: " + str(e1))
|
|
return inputList
|
|
|
|
# ---------- GeneratorController::GetSensorLogFile--------------------------
|
|
def GetSensorLogFile(self, sensor_name):
|
|
safe_name = re.sub(r'[^\w\-]', '_', sensor_name)
|
|
return os.path.join(self.SensorLogPath, "sensor_%s.txt" % safe_name)
|
|
|
|
# ---------- GeneratorController::ParseSensorTimestamp----------------------
|
|
def ParseSensorTimestamp(self, TimeStamp):
|
|
try:
|
|
struct_time = time.strptime(TimeStamp, "%x %X")
|
|
return time.mktime(struct_time)
|
|
except Exception as e1:
|
|
return 0
|
|
|
|
# ---------- GeneratorController::LogToSensorLog-------------------------------
|
|
def LogToSensorLog(self, sensor_name, TimeStamp, Value):
|
|
try:
|
|
TimeStamp = self.removeNonPrintable(TimeStamp)
|
|
Value = self.removeNonPrintable(Value)
|
|
if not len(TimeStamp) or not len(Value):
|
|
return
|
|
if not os.path.isdir(self.SensorLogPath):
|
|
os.makedirs(self.SensorLogPath)
|
|
LogFile = self.GetSensorLogFile(sensor_name)
|
|
epoch = self.ParseSensorTimestamp(TimeStamp)
|
|
cache_key = self.GetSensorCacheKey(sensor_name)
|
|
with self.SensorLogLock:
|
|
if cache_key in self.SensorLogLists and len(self.SensorLogLists[cache_key]):
|
|
self.SensorLogLists[cache_key].insert(0, [TimeStamp, Value, epoch])
|
|
self.LogToFile(LogFile, TimeStamp, Value)
|
|
# Prune if file exceeds size limit
|
|
if os.path.isfile(LogFile):
|
|
LogSize = os.path.getsize(LogFile)
|
|
if float(LogSize) / (1024 * 1024) >= self.SensorLogMaxSize * 0.85:
|
|
self.PruneSensorLog(sensor_name, 60 * 24 * 30 * 36)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in LogToSensorLog: " + str(e1))
|
|
|
|
# ---------- GeneratorController::GetSensorCacheKey-------------------------
|
|
def GetSensorCacheKey(self, sensor_name):
|
|
return re.sub(r'[^\w\-]', '_', sensor_name)
|
|
|
|
# ---------- GeneratorController::_ParseTempLogFile- -----------------------
|
|
def _ParseSensorLogFile(self, LogFile):
|
|
TempList = []
|
|
try:
|
|
with open(LogFile, "r") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not len(line):
|
|
continue
|
|
if line[0] == "#":
|
|
continue
|
|
line = self.removeNonPrintable(line)
|
|
Items = line.split(",")
|
|
if len(Items) != 2:
|
|
continue
|
|
Items[1] = self.removeAlpha(Items[1])
|
|
epoch = self.ParseSensorTimestamp(Items[0])
|
|
TempList.insert(0, [Items[0], Items[1], epoch])
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in _ParseSensorLogFile: " + str(e1))
|
|
return TempList
|
|
|
|
# ---------- GeneratorController::ReadSensorLogFromFile-----------------------
|
|
def ReadSensorLogFromFile(self, sensor_name, Minutes=0):
|
|
|
|
try:
|
|
LogFile = self.GetSensorLogFile(sensor_name)
|
|
cache_key = self.GetSensorCacheKey(sensor_name)
|
|
if not os.path.isfile(LogFile):
|
|
return []
|
|
|
|
with self.SensorLogLock:
|
|
CachedList = self.EnsureSensorCache(cache_key, LogFile)
|
|
if not Minutes:
|
|
return CachedList
|
|
# Cache is newest-first, so we can stop once we pass the cutoff
|
|
ReturnList = []
|
|
CutoffEpoch = time.time() - (Minutes * 60)
|
|
for entry in CachedList:
|
|
if entry[2] >= CutoffEpoch:
|
|
ReturnList.append(entry)
|
|
else:
|
|
break
|
|
return ReturnList
|
|
except Exception as e1:
|
|
self.LogErrorLine(f"Error in ReadSensorLogFromFile: {e1}")
|
|
return []
|
|
|
|
# ---------- GeneratorController::EnsureSensorCache---------------------------
|
|
# Populate (once) and return the cached full sensor log for a sensor.
|
|
# Caller must hold self.SensorLogLock.
|
|
def EnsureSensorCache(self, cache_key, LogFile):
|
|
if cache_key in self.SensorLogLists and len(self.SensorLogLists[cache_key]):
|
|
return self.SensorLogLists[cache_key]
|
|
TempList = self._ParseSensorLogFile(LogFile)
|
|
if len(TempList) > self.MaxSensorLogEntries:
|
|
TempList = self.DecimateList(TempList, self.MaxSensorLogEntries)
|
|
self.SensorLogLists[cache_key] = TempList
|
|
return TempList
|
|
|
|
# ---------- GeneratorController::GetSensorHistory-----------------------------
|
|
def GetSensorHistory(self, CmdString):
|
|
try:
|
|
if not len(CmdString):
|
|
self.LogError("Error in GetSensorHistory: Invalid input")
|
|
return []
|
|
|
|
# Format: "sensor_log_json=<minutes>&sensor=<name>"
|
|
CmdString = CmdString.strip()
|
|
CmdList = CmdString.split("=", 1)
|
|
if len(CmdList) != 2 or not CmdList[0].strip().lower() == "sensor_log_json":
|
|
self.LogError("Error parsing command in GetSensorHistory: " + CmdString)
|
|
return []
|
|
|
|
params = CmdList[1]
|
|
parts = params.split("&sensor=")
|
|
if len(parts) != 2:
|
|
self.LogError("Error parsing sensor in GetSensorHistory: " + CmdString)
|
|
return []
|
|
|
|
Minutes = int(parts[0].strip())
|
|
sensor_name = parts[1].strip()
|
|
# Match sensor name case-insensitively against known sensors
|
|
for known in self.GetSensorNames():
|
|
if known.lower() == sensor_name.lower():
|
|
sensor_name = known
|
|
break
|
|
|
|
TempList = self.ReadSensorLogFromFile(sensor_name, Minutes=Minutes)
|
|
if Minutes <= 360:
|
|
MaxPoints = 1000
|
|
elif Minutes <= 1440:
|
|
MaxPoints = 2000
|
|
elif Minutes <= 10080:
|
|
MaxPoints = 3000
|
|
else:
|
|
MaxPoints = 4000
|
|
if len(TempList) > MaxPoints:
|
|
TempList = self.DecimateList(TempList, MaxPoints)
|
|
# Return only [timestamp, value] for JSON response
|
|
return [[entry[0], entry[1]] for entry in TempList]
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetSensorHistory: " + str(e1))
|
|
return []
|
|
|
|
# ---------- GeneratorController::PruneSensorLog-------------------------------
|
|
def PruneSensorLog(self, sensor_name, Minutes):
|
|
try:
|
|
LogFile = self.GetSensorLogFile(sensor_name)
|
|
if not os.path.isfile(LogFile):
|
|
return
|
|
LogSize = os.path.getsize(LogFile)
|
|
if float(LogSize) / (1024 * 1024) < self.SensorLogMaxSize * 0.85:
|
|
return
|
|
if float(LogSize) / (1024 * 1024) >= self.SensorLogMaxSize:
|
|
# file too large, clear and restart
|
|
cache_key = self.GetSensorCacheKey(sensor_name)
|
|
with self.SensorLogLock:
|
|
os.remove(LogFile)
|
|
self.SensorLogLists.pop(cache_key, None)
|
|
return
|
|
# prune old entries
|
|
cache_key = self.GetSensorCacheKey(sensor_name)
|
|
CmdString = "sensor_log_json=%d&sensor=%s" % (Minutes, sensor_name)
|
|
TempLog = self.GetSensorHistory(CmdString)
|
|
with self.SensorLogLock:
|
|
if os.path.isfile(LogFile):
|
|
os.remove(LogFile)
|
|
self.SensorLogLists.pop(cache_key, None)
|
|
for Items in reversed(TempLog):
|
|
self.LogToSensorLog(sensor_name, Items[0], Items[1])
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in PruneSensorLog: " + str(e1))
|
|
|
|
# ------------ GeneratorController::ClearSensorLog---------------------------
|
|
def ClearSensorLog(self):
|
|
|
|
try:
|
|
sensors = self.GetSensorNames()
|
|
|
|
for sensor_name in sensors:
|
|
LogFile = self.GetSensorLogFile(sensor_name)
|
|
with self.SensorLogLock:
|
|
with open(LogFile, "r+") as f:
|
|
f.truncate(0)
|
|
|
|
return "Sensor Log cleared"
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ClearSensorLog: " + str(e1))
|
|
return "Error in ClearSensorLog: " + str(e1)
|
|
|
|
# ---------- GeneratorController::GetSensorNames-------------------------
|
|
def GetSensorNames(self):
|
|
try:
|
|
names = []
|
|
if self.bUseRaspberryPiCpuTempGauge:
|
|
names.append("CPU Temp")
|
|
if self.ExternalSensorGagueData:
|
|
for gauge in self.ExternalSensorGagueData:
|
|
names.append(gauge.get("title", ""))
|
|
return names
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetSensorNames: " + str(e1))
|
|
return []
|
|
|
|
# ---------- GeneratorController::SensorCollectionThread-------------------
|
|
def SensorCollectionThread(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("SensorCollectionThread", 1):
|
|
return
|
|
|
|
# if power meter is not supported do nothing.
|
|
# Note: This is done since if we killed the thread here it would log
|
|
# an error in the watchdog thead
|
|
if self.bDisableSensorLog:
|
|
self.LogError("Sensor Log Disabled.")
|
|
while self.bDisableSensorLog:
|
|
if self.WaitForExit("SensorCollectionThread", 60):
|
|
return
|
|
|
|
# Pre-warm sensor log caches so the first chart request is fast
|
|
try:
|
|
for sensor_name in self.GetSensorNames():
|
|
self.ReadSensorLogFromFile(sensor_name)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error pre-warming sensor log cache: " + str(e1))
|
|
|
|
LastCpuTemp = 0.0
|
|
|
|
while True:
|
|
try:
|
|
if self.WaitForExit("SensorCollectionThread", 10):
|
|
return
|
|
# Log CPU temperature
|
|
if self.bUseRaspberryPiCpuTempGauge and self.Platform:
|
|
CpuTemp = self.Platform.GetRaspberryPiTemp(ReturnFloat=True)
|
|
if CpuTemp != LastCpuTemp and CpuTemp != 0.0:
|
|
LastCpuTemp = CpuTemp
|
|
TimeStamp = datetime.datetime.now().strftime("%x %X")
|
|
self.LogToSensorLog("CPU Temp", TimeStamp, str(CpuTemp))
|
|
|
|
# Time to exit?
|
|
if self.IsStopSignaled("SensorCollectionThread"):
|
|
return
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SensorCollectionThread: " + str(e1))
|
|
|
|
# ---------- 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 it would log
|
|
# an error in the watchdog thead
|
|
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::SetupCommonTiles--------------------------
|
|
def SetupCommonTiles(self):
|
|
|
|
try:
|
|
|
|
if self.FuelSensorSupported():
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="Fuel Sensor",
|
|
units="%",
|
|
type="fuel",
|
|
nominal=100,
|
|
callback=self.GetFuelSensor,
|
|
callbackparameters=(True,),
|
|
)
|
|
self.TileList.append(Tile)
|
|
elif self.ExternalFuelDataSupported():
|
|
NumTanks = self.GetNumberExternalTanks()
|
|
if NumTanks >= 2:
|
|
ExternalTankTitle = "External Tank 1"
|
|
else: # only one tank
|
|
ExternalTankTitle = "External Tank"
|
|
Tile = MyTile(
|
|
self.log,
|
|
title=ExternalTankTitle,
|
|
units="%",
|
|
type="fuel",
|
|
nominal=100,
|
|
callback=self.GetExternalFuelPercentage,
|
|
callbackparameters=(True, 1),
|
|
)
|
|
self.TileList.append(Tile)
|
|
if NumTanks >= 2:
|
|
ExternalTankTitle = "External Tank 2"
|
|
Tile = MyTile(
|
|
self.log,
|
|
title=ExternalTankTitle,
|
|
units="%",
|
|
type="fuel",
|
|
nominal=100,
|
|
callback=self.GetExternalFuelPercentage,
|
|
callbackparameters=(True, 2),
|
|
)
|
|
self.TileList.append(Tile)
|
|
if NumTanks >= 3:
|
|
ExternalTankTitle = "External Tank 3"
|
|
Tile = MyTile(
|
|
self.log,
|
|
title=ExternalTankTitle,
|
|
units="%",
|
|
type="fuel",
|
|
nominal=100,
|
|
callback=self.GetExternalFuelPercentage,
|
|
callbackparameters=(True, 3),
|
|
)
|
|
self.TileList.append(Tile)
|
|
if NumTanks >= 4:
|
|
ExternalTankTitle = "External Tank 4"
|
|
Tile = MyTile(
|
|
self.log,
|
|
title=ExternalTankTitle,
|
|
units="%",
|
|
type="fuel",
|
|
nominal=100,
|
|
callback=self.GetExternalFuelPercentage,
|
|
callbackparameters=(True, 4),
|
|
)
|
|
self.TileList.append(Tile)
|
|
elif self.FuelConsumptionGaugeSupported(): # no gauge for NG
|
|
if self.UseMetric:
|
|
Units = "L" # no gauge for NG
|
|
else:
|
|
Units = "gal" # no gauge for NG
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="Estimated Fuel",
|
|
units=Units,
|
|
type="fuel",
|
|
nominal=int(self.TankSize),
|
|
callback=self.GetEstimatedFuelInTank,
|
|
callbackparameters=(True,),
|
|
)
|
|
self.TileList.append(Tile)
|
|
|
|
if self.UseExternalCTData:
|
|
NominalCurrent = float(self.NominalKW) * 1000 / self.NominalLineVolts
|
|
if int(self.Phase) == 1:
|
|
NominalLegCurrent = ((float(self.NominalKW) * 1000) / 2 ) / (self.NominalLineVolts / 2)
|
|
else:
|
|
# TODO: validate with 3 phase, also show three gauges?
|
|
NominalLegCurrent = float(self.NominalKW) * 1000 / self.NominalLineVolts
|
|
|
|
ReturnCurrent1 = self.CheckExternalCTData(request="ct1", ReturnFloat=True, gauge=True)
|
|
ReturnCurrent2 = self.CheckExternalCTData(request="ct2", ReturnFloat=True, gauge=True)
|
|
if ReturnCurrent1 != None and ReturnCurrent2 != None:
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="Current L1",
|
|
units="A",
|
|
type="current",
|
|
nominal=int(NominalLegCurrent),
|
|
callback=self.CheckExternalCTData,
|
|
callbackparameters=("ct1", True, True),
|
|
)
|
|
self.TileList.append(Tile)
|
|
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="Current L2",
|
|
units="A",
|
|
type="current",
|
|
nominal=int(NominalLegCurrent),
|
|
callback=self.CheckExternalCTData,
|
|
callbackparameters=("ct2", True, True),
|
|
)
|
|
self.TileList.append(Tile)
|
|
else:
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="External Current",
|
|
units="A",
|
|
type="current",
|
|
nominal=int(NominalCurrent),
|
|
callback=self.CheckExternalCTData,
|
|
callbackparameters=("current", True, True),
|
|
)
|
|
self.TileList.append(Tile)
|
|
NominalPower = float(self.NominalKW)
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="External Power",
|
|
units="kW",
|
|
type="power",
|
|
nominal=int(NominalPower),
|
|
callback=self.CheckExternalCTData,
|
|
callbackparameters=("power", True, True),
|
|
)
|
|
self.TileList.append(Tile)
|
|
|
|
# Raspberry pi CPU temp
|
|
if self.bUseRaspberryPiCpuTempGauge and self.Platform != None:
|
|
nominal = 80
|
|
maximum = 90
|
|
if self.UseMetric:
|
|
units = "C"
|
|
else:
|
|
units = "F"
|
|
nominal = self.ConvertCelsiusToFahrenheit(nominal)
|
|
maximum = self.ConvertCelsiusToFahrenheit(maximum)
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="CPU Temp",
|
|
units=units,
|
|
type="temperature",
|
|
subtype="cpu",
|
|
nominal=nominal,
|
|
maximum=maximum,
|
|
callback=self.Platform.GetRaspberryPiTemp,
|
|
callbackparameters=(True,),
|
|
)
|
|
self.TileList.append(Tile)
|
|
|
|
# external Temp data from gentemp add on
|
|
if self.UseExternalSensorData and self.ExternalSensorGagueData != None:
|
|
# setup each temp gauge
|
|
try:
|
|
self.LogDebug("Setting up gauges for external sensors")
|
|
index = 0
|
|
for SensorBounds in self.ExternalSensorGagueData:
|
|
sensor_max = SensorBounds['max']
|
|
if 'min' in SensorBounds:
|
|
sensor_min = SensorBounds['min']
|
|
else:
|
|
sensor_min = 0
|
|
sensor_nominal = SensorBounds['nominal']
|
|
sensor_name = SensorBounds['title'].strip()
|
|
sensor_units = SensorBounds['units']
|
|
sensor_type = SensorBounds['type']
|
|
if sensor_name != None and sensor_max != None and sensor_nominal != None:
|
|
if not SensorBounds['exclude_gauge']:
|
|
Tile = MyTile(self.log,title=sensor_name, units=sensor_units,type=sensor_type, subtype = "external",
|
|
nominal=sensor_nominal, minimum = sensor_min, maximum=sensor_max, callback=self.GetExternalSensorData, callbackparameters=(sensor_name,),)
|
|
self.TileList.append(Tile)
|
|
index += 1
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetupCommonTiles: sensor_bounds: " + str(e1))
|
|
|
|
# wifi signal strength
|
|
units = "dBm"
|
|
type = "wifi"
|
|
|
|
if self.bWifiIsPercent:
|
|
units = "%"
|
|
type = "wifipercent"
|
|
|
|
if self.bUseLinuxWifiSignalGauge and self.Platform != None:
|
|
signal = self.Platform.GetWiFiSignalStrength()
|
|
if signal != 0:
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="WiFi Signal",
|
|
units=units,
|
|
type=type,
|
|
callback=self.Platform.GetWiFiSignalStrength,
|
|
callbackparameters=(True,False,self.bWifiIsPercent),
|
|
)
|
|
self.TileList.append(Tile)
|
|
|
|
powergraphdefined = False
|
|
for tile in self.TileList:
|
|
if tile.Type == "powergraph":
|
|
powergraphdefined = True
|
|
|
|
if not powergraphdefined and self.UseExternalCTData:
|
|
# setup power graph
|
|
self.LogDebug("Setting up power graph for external CT data")
|
|
NominalPower = float(self.NominalKW)
|
|
Tile = MyTile(
|
|
self.log,
|
|
title="kW Output",
|
|
type="powergraph",
|
|
nominal=int(NominalPower),
|
|
callback=self.CheckExternalCTData,
|
|
callbackparameters=("power", True, True),
|
|
)
|
|
self.TileList.append(Tile)
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetupCommonTiles: " + str(e1))
|
|
|
|
# ---------- GeneratorController::MaintenanceHouseKeepingThread------------
|
|
def MaintenanceHouseKeepingThread(self):
|
|
|
|
try:
|
|
time.sleep(0.5)
|
|
if self.WaitForExit("MaintenanceHouseKeepingThread", 5): #
|
|
return
|
|
|
|
while True:
|
|
try:
|
|
|
|
self.KWHoursMonth = self.GetPowerHistory(
|
|
"power_log_json=43200,kw"
|
|
) # 43200 minutes in a month
|
|
self.FuelMonth = self.GetPowerHistory("power_log_json=43200,fuel")
|
|
|
|
self.RunHoursMonth = self.GetPowerHistory(
|
|
"power_log_json=43200,time"
|
|
)
|
|
|
|
if self.FuelTankCalculationSupported() and not (
|
|
self.FuelType == "Propane"
|
|
and (self.ExternalFuelDataSupported() or self.FuelSensorSupported())
|
|
):
|
|
self.EstimatedFuleInTank = self.GetEstimatedFuelInTank(ReturnFloat=True)
|
|
|
|
# Show hours of fuel remaining if any calculation is supported
|
|
if (
|
|
self.FuelTankCalculationSupported()
|
|
or self.ExternalFuelDataSupported()
|
|
or self.FuelSensorSupported()
|
|
):
|
|
self.HoursFuelRemainingAtLoad = self.GetRemainingFuelTime(ReturnFloat=True)
|
|
|
|
self.HoursFuelRemainingCurrentLoad = self.GetRemainingFuelTime(
|
|
ReturnFloat=True, Actual=True
|
|
)
|
|
if self.WaitForExit("MaintenanceHouseKeepingThread", 60 * 10): #
|
|
return
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in MaintenanceHouseKeepingThread: " + str(e1))
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in MaintenanceHouseKeepingThread (2): " + str(e1))
|
|
|
|
# ---------- GeneratorController::DisplayMaintenanceCommon------------------
|
|
def DisplayMaintenanceCommon(self, Maintenance, JSONNum=False):
|
|
|
|
try:
|
|
if self.UseMetric:
|
|
Units = "L"
|
|
else:
|
|
Units = "gal"
|
|
|
|
if self.FuelSensorSupported():
|
|
FuelValue = self.GetFuelSensor(ReturnInt=True)
|
|
Maintenance["Maintenance"].append(
|
|
{"Fuel Level Sensor": self.ValueOut(FuelValue, "%", JSONNum)}
|
|
)
|
|
FuelValue = self.GetFuelInTank(ReturnFloat=True)
|
|
if FuelValue != None:
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Fuel In Tank (Sensor)": self.ValueOut(
|
|
FuelValue, Units, JSONNum
|
|
)
|
|
}
|
|
)
|
|
elif self.ExternalFuelDataSupported():
|
|
NumTanks = self.GetNumberExternalTanks()
|
|
if NumTanks >= 1:
|
|
FuelValue = self.GetExternalFuelPercentage(
|
|
ReturnFloat=True, TankNumber=1
|
|
)
|
|
Maintenance["Maintenance"].append(
|
|
{"Fuel Level Sensor": self.ValueOut(FuelValue, "%", JSONNum)}
|
|
)
|
|
if NumTanks >= 2:
|
|
FuelValue = self.GetExternalFuelPercentage(
|
|
ReturnFloat=True, TankNumber=2
|
|
)
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Fuel Level Sensor Tank 2": self.ValueOut(
|
|
FuelValue, "%", JSONNum
|
|
)
|
|
}
|
|
)
|
|
if NumTanks >= 3:
|
|
FuelValue = self.GetExternalFuelPercentage(
|
|
ReturnFloat=True, TankNumber=3
|
|
)
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Fuel Level Sensor Tank 3": self.ValueOut(
|
|
FuelValue, "%", JSONNum
|
|
)
|
|
}
|
|
)
|
|
if NumTanks >= 4:
|
|
FuelValue = self.GetExternalFuelPercentage(
|
|
ReturnFloat=True, TankNumber=4
|
|
)
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Fuel Level Sensor Tank 4": self.ValueOut(
|
|
FuelValue, "%", JSONNum
|
|
)
|
|
}
|
|
)
|
|
FuelValue = self.GetFuelInTank(ReturnFloat=True)
|
|
if FuelValue != None:
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Fuel In Tank (Sensor)": self.ValueOut(
|
|
FuelValue, Units, JSONNum
|
|
)
|
|
}
|
|
)
|
|
|
|
# Don't Show estimated fuel for propane tanks with a sensor on Evo controllers
|
|
if self.FuelTankCalculationSupported() and not (
|
|
self.FuelType == "Propane"
|
|
and (self.ExternalFuelDataSupported() or self.FuelSensorSupported())
|
|
):
|
|
if self.EstimatedFuleInTank != None:
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Estimated Fuel In Tank ": self.ValueOut(
|
|
self.EstimatedFuleInTank,
|
|
Units,
|
|
JSONNum,
|
|
)
|
|
}
|
|
)
|
|
|
|
# Show hours of fuel remaining if any calculation is supported
|
|
if (
|
|
self.FuelTankCalculationSupported()
|
|
or self.ExternalFuelDataSupported()
|
|
or self.FuelSensorSupported()
|
|
):
|
|
if self.HoursFuelRemainingAtLoad != None:
|
|
DisplayText = (
|
|
"Hours of Fuel Remaining (Estimated %.02f Load )"
|
|
% self.EstimateLoad
|
|
)
|
|
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
DisplayText: self.ValueOut(
|
|
self.HoursFuelRemainingAtLoad, "h", JSONNum
|
|
)
|
|
}
|
|
)
|
|
|
|
if self.HoursFuelRemainingCurrentLoad != None:
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Hours of Fuel Remaining (Current Load)": self.ValueOut(
|
|
self.HoursFuelRemainingCurrentLoad, "h", JSONNum
|
|
)
|
|
}
|
|
)
|
|
|
|
if self.PowerMeterIsSupported() and self.FuelConsumptionSupported():
|
|
if self.KWHoursMonth != None:
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"kW Hours in last 30 days": self.UnitsOut(
|
|
str(self.KWHoursMonth) + " kWh",
|
|
type=float,
|
|
NoString=JSONNum,
|
|
)
|
|
}
|
|
)
|
|
if self.FuelMonth != None:
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Fuel Consumption in last 30 days": self.UnitsOut(
|
|
self.FuelMonth, type=float, NoString=JSONNum
|
|
)
|
|
}
|
|
)
|
|
if self.RunHoursMonth != None:
|
|
Maintenance["Maintenance"].append(
|
|
{
|
|
"Run Hours in last 30 days": self.UnitsOut(
|
|
str(self.RunHoursMonth) + " h",
|
|
type=float,
|
|
NoString=JSONNum,
|
|
)
|
|
}
|
|
)
|
|
|
|
if self.FuelLevelOK != None:
|
|
if self.FuelLevelOK:
|
|
level = "OK"
|
|
else:
|
|
level = "Low"
|
|
Maintenance["Maintenance"].append({"Fuel Level State": level})
|
|
|
|
return Maintenance
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayMaintenanceCommon: " + str(e1))
|
|
return Maintenance
|
|
|
|
# ---------- GeneratorController::DisplayStatusCommon-----------------------
|
|
def DisplayStatusCommon(self, Status, JSONNum=False):
|
|
|
|
try:
|
|
|
|
with self.ExternalDataLock:
|
|
try:
|
|
if self.ExternalSensorData != None:
|
|
if not JSONNum:
|
|
Status["Status"].append({"External Sensors":self.ExternalSensorData})
|
|
else:
|
|
# This will unpack the list of dicts and put them in the JSONNum format
|
|
TempList = []
|
|
ExternalTempList = self.ExternalSensorData
|
|
if len(ExternalTempList):
|
|
Status["Status"].append({"External Sensors" : TempList})
|
|
for ExTempDict in ExternalTempList:
|
|
try:
|
|
TempDict = ExTempDict.copy() # make a copy since we use popitmes below
|
|
if len(TempDict):
|
|
TempKey, TempData = TempDict.popitem()
|
|
TempDataList = TempData.strip().split( " ")
|
|
TempList.append({TempKey: self.ValueOut(float(TempDataList[0]), TempDataList[1], JSONNum)})
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayStatus (3): " + str(e1))
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayStatusCommon(2): " + str(e1))
|
|
|
|
ReturnCurrent = self.CheckExternalCTData(request="current", ReturnFloat=True, gauge=True)
|
|
ReturnCurrent1 = self.CheckExternalCTData(request="ct1", ReturnFloat=True, gauge=True)
|
|
ReturnCurrent2 = self.CheckExternalCTData(request="ct2", ReturnFloat=True, gauge=True)
|
|
ReturnPower = self.CheckExternalCTData(request="power", ReturnFloat=True, gauge=True)
|
|
ReturnPower1 = self.CheckExternalCTData(request="ctpower1", ReturnFloat=True, gauge=True)
|
|
ReturnPower2 = self.CheckExternalCTData(request="ctpower2", ReturnFloat=True, gauge=True)
|
|
|
|
if ReturnCurrent != None and ReturnPower != None:
|
|
ExternalSensors = []
|
|
Status["Status"].append({"External Line Sensors": ExternalSensors})
|
|
|
|
if ReturnCurrent != None:
|
|
ExternalSensors.append(
|
|
{"Total Current": self.ValueOut(ReturnCurrent, "A", JSONNum)}
|
|
)
|
|
if ReturnCurrent1 != None:
|
|
ExternalSensors.append(
|
|
{"Current Leg 1": self.ValueOut(ReturnCurrent1, "A", JSONNum)}
|
|
)
|
|
if ReturnCurrent2 != None:
|
|
ExternalSensors.append(
|
|
{"Current Leg 2": self.ValueOut(ReturnCurrent2, "A", JSONNum)}
|
|
)
|
|
if ReturnPower != None:
|
|
ExternalSensors.append(
|
|
{"Power": self.ValueOut(ReturnPower, "kW", JSONNum)}
|
|
)
|
|
if ReturnPower1 != None:
|
|
ExternalSensors.append(
|
|
{"Power Leg 1": self.ValueOut(ReturnPower1, "kW", JSONNum)}
|
|
)
|
|
if ReturnPower2 != None:
|
|
ExternalSensors.append(
|
|
{"Power Leg 2": self.ValueOut(ReturnPower2, "kW", JSONNum)}
|
|
)
|
|
return Status
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayStatusCommon: " + str(e1))
|
|
return Status
|
|
|
|
# ---------- GeneratorController::GetFuelInTank-----------------------------
|
|
def GetFuelInTank(self, ReturnFloat=False):
|
|
|
|
try:
|
|
if self.TankSize == 0:
|
|
return None
|
|
|
|
if self.UseMetric:
|
|
Units = "L"
|
|
else:
|
|
Units = "gal"
|
|
|
|
FuelLevel = self.GetFuelLevel(ReturnFloat=True)
|
|
if FuelLevel == None:
|
|
return None
|
|
FuelLevel = (FuelLevel * 0.01) * float(self.TankSize)
|
|
|
|
if ReturnFloat:
|
|
return float(FuelLevel)
|
|
else:
|
|
return "%.2f %s" % (FuelLevel, Units)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetFuelInTank: " + str(e1))
|
|
return None
|
|
|
|
# ---------- GeneratorController::GetFuelLevel------------------------------
|
|
def GetFuelLevel(self, ReturnFloat=False):
|
|
# return 0 - 100 or None
|
|
|
|
if (
|
|
not self.ExternalFuelDataSupported()
|
|
and not self.FuelTankCalculationSupported()
|
|
and not self.FuelSensorSupported()
|
|
):
|
|
return None
|
|
|
|
if self.FuelSensorSupported():
|
|
FuelLevel = self.GetFuelSensor(ReturnInt=True)
|
|
if FuelLevel == None:
|
|
return None
|
|
FuelLevel = float(FuelLevel)
|
|
elif self.ExternalFuelDataSupported():
|
|
FuelLevel = self.GetExternalFuelPercentage(ReturnFloat=True)
|
|
elif self.FuelTankCalculationSupported():
|
|
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
|
|
else:
|
|
FuelLevel = 0
|
|
if FuelLevel == None:
|
|
return FuelLevel
|
|
if ReturnFloat:
|
|
return float(FuelLevel)
|
|
else:
|
|
return "%.2f %%" % FuelLevel
|
|
|
|
# ---------- GeneratorController::CheckFuelLevel----------------------------
|
|
def CheckFuelLevel(self):
|
|
try:
|
|
if (
|
|
not self.ExternalFuelDataSupported()
|
|
and 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%. Fuel Level is "
|
|
+ str(FuelLevel)
|
|
+ "%"
|
|
)
|
|
title = "Warning: Fuel Level Low (10%) at " + self.SiteName
|
|
self.MessagePipe.SendMessage(
|
|
title, msgbody, msgtype="warn", onlyonce=True
|
|
)
|
|
self.FuelLevelOK = False
|
|
return False
|
|
elif FuelLevel <= 20: # 20 percent left
|
|
msgbody = (
|
|
"Warning: The estimated fuel in the tank is at or below 20%. Fuel Level is "
|
|
+ str(FuelLevel)
|
|
+ "%"
|
|
)
|
|
title = "Warning: Fuel Level Low (20%) at " + self.SiteName
|
|
self.MessagePipe.SendMessage(
|
|
title, msgbody, msgtype="warn", onlyonce=True
|
|
)
|
|
self.FuelLevelOK = False
|
|
return False
|
|
else:
|
|
self.FuelLevelOK = True
|
|
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
|
|
|
|
# ------------ GeneratorController:GetFuelSensor ---------------------------
|
|
def GetFuelSensor(self, ReturnInt=False):
|
|
return None
|
|
|
|
# ---------- GeneratorController::FuelSensorSupported------------------------
|
|
def FuelSensorSupported(self):
|
|
return False
|
|
|
|
# ---------- GeneratorController::FuelTankCalculationSupported--------------
|
|
def FuelTankCalculationSupported(self):
|
|
|
|
if not self.PowerMeterIsSupported():
|
|
return False
|
|
if not self.FuelConsumptionSupported():
|
|
return False
|
|
|
|
if self.TankSize == 0:
|
|
return False
|
|
|
|
if self.FuelType == "Natural Gas":
|
|
return False
|
|
return True
|
|
|
|
# ---------- GeneratorController::FuelConsumptionSupported------------------
|
|
def FuelConsumptionSupported(self):
|
|
|
|
if self.GetFuelConsumptionDataPoints() == None:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
# ---------- GeneratorController::FuelConsumptionGaugeSupported-------------
|
|
def FuelConsumptionGaugeSupported(self):
|
|
|
|
if self.FuelTankCalculationSupported() and self.FuelType != "Natural Gas":
|
|
return True
|
|
return False
|
|
|
|
# ---------- GeneratorController::GetRemainingFuelTime------------------------
|
|
def GetRemainingFuelTime(self, ReturnFloat=False, Actual=False):
|
|
|
|
try:
|
|
if not self.FuelConsumptionGaugeSupported():
|
|
return None
|
|
if (
|
|
not self.ExternalFuelDataSupported()
|
|
and not self.FuelTankCalculationSupported()
|
|
and not self.FuelSensorSupported()
|
|
):
|
|
return None
|
|
if self.TankSize == 0:
|
|
return None
|
|
|
|
FuelLevel = self.GetFuelLevel(ReturnFloat=True)
|
|
FuelRemaining = self.TankSize * (FuelLevel / 100.0)
|
|
|
|
if Actual:
|
|
PowerValue = self.GetPowerOutput(ReturnFloat=True)
|
|
else:
|
|
PowerValue = self.EstimateLoad * float(self.NominalKW)
|
|
|
|
if PowerValue == 0:
|
|
return None
|
|
|
|
FuelPerHour, Units = self.GetFuelConsumption(PowerValue, 60 * 60)
|
|
if FuelPerHour == None or not len(Units):
|
|
return None
|
|
if FuelPerHour == 0:
|
|
return None
|
|
|
|
try:
|
|
# make sure our units are correct
|
|
# 1 cubic foot propane = 0.0278 gallons propane
|
|
# 1 gallon propane = 35.97 cubic feet propane
|
|
# 1 cubic foot natural gas = 0.012 gallons natural gas
|
|
# 1 gallon natural gas = 82.62 cubic feet natural gas
|
|
if Units.lower() == "cubic feet" and self.UseMetric == False:
|
|
# this means that fuel left is gallons and fuel per hour is cubic feet
|
|
# so convert remaining fuel from gallons to cu ft
|
|
if self.FuelType == "Natural Gas":
|
|
# 1 gallon natural gas = 82.62 cubic feet natural gas
|
|
FuelRemaining = FuelRemaining * 82.62
|
|
else:
|
|
# 1 gallon propane = 35.97 cubic feet propane
|
|
FuelRemaining = FuelRemaining * 35.97
|
|
elif Units.lower() == "gal" and self.UseMetric == True:
|
|
# this mean that fuel left is Liters, and fuel per hour is gallons
|
|
# so convert remaining fuel to gallons
|
|
# 1 L = 0.264172 gal
|
|
FuelRemaining = FuelRemaining * 0.264172
|
|
elif Units.lower() == "cubic feet" and self.UseMetric == True:
|
|
# this means that fuel left is Liters and fuel per hour is cu feet
|
|
# so convert remaing fuel to cubic feet
|
|
# 1 L = 0.0353147 cu ft
|
|
FuelRemaining = FuelRemaining * 0.0353147
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetRemainingFuelTime (2): " + str(e1))
|
|
|
|
HoursRemaining = FuelRemaining / FuelPerHour
|
|
|
|
if ReturnFloat:
|
|
return float(HoursRemaining)
|
|
else:
|
|
return "%.2f h" % HoursRemaining
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetRemainingFuelTime: " + str(e1))
|
|
return None
|
|
|
|
# ---------- GeneratorController::GetFuelConsumption------------------------
|
|
def GetFuelConsumption(self, kw, seconds):
|
|
try:
|
|
ConsumptionData = self.GetFuelConsumptionDataPoints()
|
|
|
|
if ConsumptionData == None or len(ConsumptionData) != 5:
|
|
self.LogDebug("ERROR: Invalid fuel consumption data in GetFuelConsumption: " + str(ConsumptionData))
|
|
return None, ""
|
|
|
|
if self.NominalKW == None or float(self.NominalKW) == 0.0:
|
|
self.LogDebug("ERROR: Invalid nominal kW in GetFuelConsumption: " + str(self.NominalKW))
|
|
return None, ""
|
|
|
|
if kw < 0:
|
|
self.LogDebug("WARNING: Load is %d in GetFuelConsumption" % kw)
|
|
kw = 0
|
|
if seconds < 0:
|
|
self.LogDebug("WARNING: seconds is %d in GetFuelConsumption" % seconds)
|
|
seconds = 0
|
|
|
|
Load = kw / float(self.NominalKW)
|
|
# X axis is percent utilization, y axis is fuel consumption
|
|
X1 = ConsumptionData[0]
|
|
Y1 = ConsumptionData[1]
|
|
X2 = ConsumptionData[2]
|
|
Y2 = ConsumptionData[3]
|
|
Units = ConsumptionData[4]
|
|
|
|
if seconds == 0:
|
|
# for zero duration return zero fuel used
|
|
if self.UseMetric and self.FuelType == "Natural Gas":
|
|
return 0.0, "cubic meters"
|
|
elif self.UseMetric:
|
|
return 0.0, "L"
|
|
else:
|
|
0.0, Units
|
|
# Slope of fuel consumption plot (it is very close to if not linear in most cases)
|
|
Slope = (Y2 - Y1) / (X2 - X1)
|
|
# now use point slope equation to find consumption for one hour
|
|
# percent load is X2, Consumption is Y2, 100% (1.0) is X1 and Rate 100% is Y1
|
|
# Y1-Y2= SLOPE(X1-X2)
|
|
X2 = Load
|
|
Y2 = (((Slope * X1) - (Slope * X2)) - Y1) * -1
|
|
Consumption = Y2
|
|
|
|
# now compensate for time
|
|
Consumption = (seconds / 3600) * Consumption
|
|
|
|
if Consumption < 0:
|
|
self.LogDebug("WARNING: Fuel Consumption is " + str(Consumption) + " " + Units)
|
|
Consumption = 0
|
|
|
|
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), Units
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetFuelConsumption: " + str(e1))
|
|
return None, ""
|
|
|
|
# ---------- GeneratorController::GetFuelConsumptionDataPoints--------------
|
|
def GetFuelConsumptionDataPoints(self):
|
|
|
|
# Data points are expressed in a list [.50,50% fuel rate,1.0, 100% fuel rate, units]
|
|
|
|
# The general rule of thumb for fuel consumption for diesel is 7% of the
|
|
# rated generator output (Example: 200 kW x 7% = 1.4 gallon per hour at full load).
|
|
# For Larger diesel generators KW * 7% = Fuel per hour
|
|
# for 60 kw and below diesle generators KW * 8.5% = Fuel per hour
|
|
try:
|
|
if self.FuelHalfRate == 0 or self.FuelFullRate == 0:
|
|
self.LogDebug("ERROR: Fuel Half Rate or Full Rate is zero: Half: " + str(self.FuelHalfRate) + "Full: " + str(self.FuelFullRate) )
|
|
return None
|
|
|
|
return [
|
|
0.5,
|
|
float(self.FuelHalfRate),
|
|
1.0,
|
|
float(self.FuelFullRate),
|
|
self.FuelUnits,
|
|
]
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetFuelConsumptionDataPoints: " + str(e1))
|
|
return None
|
|
|
|
# ---------- GeneratorController::ExternalFuelDataSupported-----------------
|
|
def ExternalFuelDataSupported(self):
|
|
return self.UseExternalFuelData
|
|
|
|
# ---------- GeneratorController::GetNumberExternalTanks--------------------
|
|
def GetNumberExternalTanks(self):
|
|
|
|
try:
|
|
if not self.ExternalFuelDataSupported():
|
|
return 0
|
|
if self.TankData == None:
|
|
return 0
|
|
if "Percentage4" in self.TankData:
|
|
return 4
|
|
elif "Percentage3" in self.TankData:
|
|
return 3
|
|
elif "Percentage2" in self.TankData:
|
|
return 2
|
|
elif "Percentage" in self.TankData:
|
|
return 1
|
|
else:
|
|
return 0
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetNumberExternalTanks: " + str(e1))
|
|
return 0
|
|
|
|
# ---------- GeneratorController::GetExternalFuelPercentage-----------------
|
|
def GetExternalFuelPercentage(self, ReturnFloat=False, TankNumber=0):
|
|
|
|
try:
|
|
if ReturnFloat:
|
|
DefaultReturn = 0.0
|
|
else:
|
|
DefaultReturn = "0"
|
|
|
|
if not self.ExternalFuelDataSupported():
|
|
return DefaultReturn
|
|
|
|
if self.TankData == None:
|
|
return DefaultReturn
|
|
|
|
NumTanks = self.GetNumberExternalTanks()
|
|
if TankNumber > NumTanks:
|
|
return DefaultReturn
|
|
|
|
if TankNumber == 1:
|
|
percentage = self.TankData["Percentage"]
|
|
if TankNumber == 2:
|
|
percentage = self.TankData["Percentage2"]
|
|
if TankNumber == 3:
|
|
percentage = self.TankData["Percentage3"]
|
|
if TankNumber == 4:
|
|
percentage = self.TankData["Percentage4"]
|
|
# TankNumber == 0 denotes an average of all tanks
|
|
if TankNumber == 0:
|
|
if "Percentage4" in self.TankData:
|
|
percentage = (
|
|
float(self.TankData["Percentage"])
|
|
+ float(self.TankData["Percentage2"])
|
|
+ float(self.TankData["Percentage3"])
|
|
+ float(self.TankData["Percentage4"])
|
|
) / 4
|
|
elif "Percentage3" in self.TankData:
|
|
percentage = (
|
|
float(self.TankData["Percentage"])
|
|
+ float(self.TankData["Percentage2"])
|
|
+ float(self.TankData["Percentage3"])
|
|
) / 3
|
|
elif "Percentage2" in self.TankData:
|
|
percentage = (
|
|
float(self.TankData["Percentage"])
|
|
+ float(self.TankData["Percentage2"])
|
|
) / 2
|
|
else:
|
|
percentage = self.TankData["Percentage"]
|
|
if ReturnFloat:
|
|
return float(percentage)
|
|
else:
|
|
return str(percentage)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetExternalFuelPercentage: " + str(e1))
|
|
return DefaultReturn
|
|
|
|
# ---------- GeneratorController::SetExternalTankData-----------------------
|
|
def SetExternalTankData(self, command):
|
|
|
|
try:
|
|
bInitTiles = False
|
|
CmdList = command.split("=")
|
|
if len(CmdList) == 2:
|
|
with self.ExternalDataLock:
|
|
if self.TankData == None:
|
|
bInitTiles = True
|
|
self.TankData = json.loads(CmdList[1])
|
|
if bInitTiles:
|
|
self.UseExternalFuelData = True
|
|
self.SetupTiles()
|
|
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::SetExternalSensorData----------------
|
|
def SetExternalSensorData(self, command):
|
|
|
|
try:
|
|
if not isinstance(command, str) and not isinstance(command, unicode):
|
|
self.LogErrorLine("Error in SetExternalSensorData, invalid data: " + str(type(command)))
|
|
return "Error"
|
|
|
|
with self.ExternalDataLock:
|
|
CmdList = command.split("=")
|
|
if len(CmdList) == 2:
|
|
if self.ExternalSensorData == None:
|
|
self.ExternalSensorData = json.loads(CmdList[1])
|
|
else:
|
|
new_list = json.loads(CmdList[1]) # list of dicts {'label': 'value with units'}
|
|
for new_sensor in new_list:
|
|
found = False
|
|
for i in range(len(self.ExternalSensorData)):
|
|
existing_sensor = self.ExternalSensorData[i]
|
|
if list(new_sensor.keys())[0] == list(existing_sensor.keys())[0]:
|
|
self.ExternalSensorData[i] = new_sensor
|
|
found = True
|
|
if not found: # first time seeing this sensor
|
|
self.ExternalSensorData.append(new_sensor)
|
|
|
|
self.UseExternalSensorData = True
|
|
self.ExternalSensorDataTime = datetime.datetime.now()
|
|
|
|
# Log temperature data to per-sensor files
|
|
TimeStamp = datetime.datetime.now().strftime("%x %X")
|
|
for sensor_dict in json.loads(CmdList[1]):
|
|
for sensor_name, sensor_value in sensor_dict.items():
|
|
numeric_value = str(self.ConvertToNumber(sensor_value))
|
|
self.LogToSensorLog(sensor_name, TimeStamp, numeric_value)
|
|
|
|
else:
|
|
self.LogError("Error in SetExternalSensorData: invalid input: " + str(len(CmdList)))
|
|
return "Error"
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetExternalSensorData: " + str(e1))
|
|
return "Error"
|
|
|
|
return "OK"
|
|
|
|
# ---------- GeneratorController::GetExternalSensorData--------------------------
|
|
# used to get external temp data for gauge / tile
|
|
def GetExternalSensorData(self, sensor_name):
|
|
try:
|
|
if not self.UseExternalSensorData or self.ExternalSensorData == None or self.ExternalSensorGagueData == None:
|
|
return 0.0
|
|
|
|
with self.ExternalDataLock:
|
|
if len(self.ExternalSensorData) > 0:
|
|
for SensorDict in self.ExternalSensorData:
|
|
if sensor_name == list(SensorDict.keys())[0]:
|
|
sensor_value = SensorDict[sensor_name]
|
|
return self.ConvertToNumber(sensor_value)
|
|
else:
|
|
self.LogDebug("Error in GetExternalSensorData: " + str(self.ExternalSensorData))
|
|
|
|
self.LogDebug("Sensor data not found in GetExternalSensorData: " + str(sensor_name))
|
|
|
|
return 0.0
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetExternalSensorData: " + str(e1) + ": " + str(sensor_name) + ": " + str(self.ExternalSensorData))
|
|
return 0.0
|
|
|
|
# ---------- GeneratorController::SetExternalGaugeData-------------
|
|
# NOTE: This command must be called first before SetExternalSensorData
|
|
# otherwise the bounds data will be ignored
|
|
def SetExternalGaugeData(self, command):
|
|
|
|
try:
|
|
if not isinstance(command, str) and not isinstance(command, unicode):
|
|
self.LogErrorLine("Error in SetExternalGaugeData, invalid data: " + str(type(command)))
|
|
return "Error"
|
|
|
|
with self.ExternalDataLock:
|
|
CmdList = command.split("=")
|
|
if len(CmdList) == 2:
|
|
TempSensorGaugeList = json.loads(CmdList[1])
|
|
if self.ExternalSensorGagueData == None:
|
|
self.ExternalSensorGagueData = TempSensorGaugeList
|
|
self.UseExternalSensorData = True
|
|
else:
|
|
|
|
for Gauge in TempSensorGaugeList:
|
|
found = False
|
|
for ExistingGauge in self.ExternalSensorGagueData:
|
|
if ExistingGauge['title'] == Gauge['title']:
|
|
found = True # already exist
|
|
self.LogDebug("Gauge already exist: " + Gauge['title'])
|
|
break
|
|
if not found:
|
|
self.ExternalSensorGagueData.append(Gauge)
|
|
self.SetupTiles()
|
|
else:
|
|
self.LogError("Error in SetExternalGaugeData: invalid input: " + str(len(CmdList)))
|
|
return "Error"
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetExternalGaugeData: " + str(e1))
|
|
return "Error"
|
|
|
|
return "OK"
|
|
# ---------- GeneratorController::SetExternalCTData-------------------------
|
|
def SetExternalCTData(self, command):
|
|
try:
|
|
bInitTiles = False
|
|
CmdList = command.split("=")
|
|
if len(CmdList) == 2:
|
|
with self.ExternalDataLock:
|
|
if self.ExternalCTData == None:
|
|
bInitTiles = True
|
|
self.ExternalCTData = json.loads(CmdList[1])
|
|
if bInitTiles:
|
|
self.UseExternalCTData = True
|
|
self.SetupTiles()
|
|
else:
|
|
self.LogError("Error in SetExternalTankData: invalid input")
|
|
return "Error"
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetExternalCTData: " + str(e1))
|
|
return "Error"
|
|
return "OK"
|
|
|
|
# ---------- GeneratorController::GetExternalCTData-------------------------
|
|
def GetExternalCTData(self):
|
|
try:
|
|
if not self.UseExternalCTData:
|
|
return None
|
|
if self.ExternalCTData != None:
|
|
with self.ExternalDataLock:
|
|
return self.ExternalCTData.copy()
|
|
else:
|
|
return None
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetExternalCTData: " + str(e1))
|
|
return None
|
|
return None
|
|
|
|
# ------------ GeneratorController:ConvertExternalData ---------------------
|
|
def ConvertExternalData(self, request="current", voltage=None, ReturnFloat=False):
|
|
|
|
try:
|
|
# ExternalData can contain any of the following: current, power, ctdata[], ctpower[], voltage
|
|
# ctpower[] and ctdata[] is a list of 2 ints, voltage is both legs combined
|
|
if not self.UseExternalCTData:
|
|
return None
|
|
ExternalData = self.GetExternalCTData()
|
|
|
|
if ExternalData == None:
|
|
return None
|
|
|
|
if request.lower() == "current" and "current" in ExternalData.keys():
|
|
return self.ReturnFormat(ExternalData["current"], "A", ReturnFloat)
|
|
|
|
if request.lower() == "power" and "power" in ExternalData.keys():
|
|
return self.ReturnFormat(ExternalData["power"], "kW", ReturnFloat)
|
|
|
|
if (
|
|
request.lower() == "ct1"
|
|
and "ctdata" in ExternalData.keys()
|
|
and len(ExternalData["ctdata"]) >= 2
|
|
):
|
|
return self.ReturnFormat(ExternalData["ctdata"][0], "A", ReturnFloat)
|
|
if (
|
|
request.lower() == "ct2"
|
|
and "ctdata" in ExternalData.keys()
|
|
and len(ExternalData["ctdata"]) >= 2
|
|
):
|
|
return self.ReturnFormat(ExternalData["ctdata"][1], "A", ReturnFloat)
|
|
if (
|
|
request.lower() == "ctpower1"
|
|
and "ctpower" in ExternalData.keys()
|
|
and len(ExternalData["ctpower"]) >= 2
|
|
):
|
|
return self.ReturnFormat(ExternalData["ctpower"][0], "kW", ReturnFloat)
|
|
if (
|
|
request.lower() == "ctpower2"
|
|
and "ctpower" in ExternalData.keys()
|
|
and len(ExternalData["ctpower"]) >= 2
|
|
):
|
|
return self.ReturnFormat(ExternalData["ctpower"][1], "kW", ReturnFloat)
|
|
|
|
if "powerfactor" in ExternalData.keys():
|
|
powerfactor = float(ExternalData["powerfactor"])
|
|
else:
|
|
powerfactor = 1.0
|
|
|
|
if voltage == None:
|
|
if "voltage" in ExternalData.keys():
|
|
voltage = int(ExternalData["voltage"])
|
|
else:
|
|
return None
|
|
|
|
if "phase" in ExternalData.keys():
|
|
phase = ExternalData["phase"]
|
|
else:
|
|
phase = 1
|
|
if phase == 1:
|
|
singlelegvoltage = voltage / 2
|
|
else:
|
|
# TODO check this
|
|
singlelegvoltage = voltage / 3
|
|
|
|
if request.lower() == "current" and "power" in ExternalData.keys():
|
|
if voltage == 0:
|
|
return self.ReturnFormat(0.0, "A", ReturnFloat)
|
|
|
|
if "ctpower" in ExternalData.keys() and len(ExternalData["ctpower"]) >= 2:
|
|
power1 = float(ExternalData["ctpower"][0]) * 1000
|
|
power2 = float(ExternalData["ctpower"][1]) * 1000
|
|
CurrentFloat = round(
|
|
(power1 / (powerfactor * (singlelegvoltage)))
|
|
+ (power2 / (powerfactor * (singlelegvoltage))),
|
|
2,
|
|
)
|
|
else:
|
|
PowerFloat = float(ExternalData["power"]) * 1000.0
|
|
# I(A) = P(W) / (PF x V(V))
|
|
CurrentFloat = round(PowerFloat / (powerfactor * voltage), 2)
|
|
return self.ReturnFormat(CurrentFloat, "A", ReturnFloat)
|
|
elif request.lower() == "power"and "current" in ExternalData.keys() and "ctdata" in ExternalData.keys():
|
|
CurrentFloat = float(ExternalData["current"])
|
|
if len(ExternalData["ctdata"]) < 2:
|
|
# P(W) = PF x I(A) x V(V)
|
|
PowerFloat = (powerfactor * CurrentFloat * voltage) / 1000
|
|
else:
|
|
# P(W) = (PF x I(A) x V(V)) + (PF x I(A) x V(V))
|
|
current1 = float(ExternalData["ctdata"][0])
|
|
current2 = float(ExternalData["ctdata"][1])
|
|
PowerFloat = (
|
|
(powerfactor * current1 * (singlelegvoltage))
|
|
+ (powerfactor * current2 * (singlelegvoltage))
|
|
) / 1000
|
|
#
|
|
return self.ReturnFormat(PowerFloat, "kW", ReturnFloat)
|
|
elif (request.lower() in ["ct1", "ct2"] and "ctpower" in ExternalData.keys()
|
|
and "voltagelegs" in ExternalData.keys() and len(ExternalData["voltagelegs"]) != 0):
|
|
# convert leg power to leg current
|
|
if request.lower() == "ct1":
|
|
ctpower = ExternalData["ctpower"][0]
|
|
extvolts = ExternalData["voltagelegs"][0]
|
|
elif request.lower() == "ct2":
|
|
ctpower = ExternalData["ctpower"][1]
|
|
extvolts = ExternalData["voltagelegs"][1]
|
|
else:
|
|
self.LogDebug("Error in ConvertExternalData: invalid CT requested")
|
|
return None
|
|
CurrentFloat = round(((ctpower*1000.0) / extvolts),2)
|
|
return self.ReturnFormat(CurrentFloat, "A", ReturnFloat)
|
|
elif (request.lower() in ["ctpower1", "ctpower2"] and "ctdata" in ExternalData.keys()
|
|
and "voltagelegs" in ExternalData.keys() and len(ExternalData["voltagelegs"]) != 0):
|
|
# convert leg current to leg power
|
|
if request.lower() == "ctpower1":
|
|
extcurrent = ExternalData["ctdata"][0]
|
|
extvolts = ExternalData["voltagelegs"][0]
|
|
elif request.lower() == "ctpower2":
|
|
extcurrent = ExternalData["ctdata"][1]
|
|
extvolts = ExternalData["voltagelegs"][1]
|
|
else:
|
|
self.LogDebug("Error in ConvertExternalData: invalid Power requested")
|
|
return None
|
|
PowerFloat = round(((extcurrent * extvolts)/1000.0),2)
|
|
return self.ReturnFormat(PowerFloat, "kW", ReturnFloat)
|
|
return None
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ConvertExternalData: " + str(e1))
|
|
return None
|
|
|
|
# ------------ GeneratorController:ReturnFormat ----------------------------
|
|
def ReturnFormat(sefl, value, units, ReturnFloat):
|
|
|
|
if ReturnFloat:
|
|
return round(float(value), 2)
|
|
else:
|
|
return ("%.2f " + units) % float(value)
|
|
|
|
# ---------- 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)
|
|
outfile.flush()
|
|
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::GetRegisterLabels------------------------
|
|
def GetRegisterLabels(self):
|
|
# return JSON of dict with registers and text descriptions
|
|
try:
|
|
return "{}" # this is the default, no labels
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetRegisterLabels: " + str(e1))
|
|
return "{}"
|
|
|
|
# ---------- GeneratorController::GetMaintLogJSON--------------------------
|
|
def GetMaintLogJSON(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 GetMaintLogJSON: " + str(e1))
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetMaintLogJSON (2): " + str(e1))
|
|
|
|
return "[]"
|
|
|
|
# ---------- GeneratorController::GetMaintLogDict--------------------------
|
|
def GetMaintLogDict(self):
|
|
try:
|
|
if len(self.MaintLogList):
|
|
return self.MaintLogList
|
|
if os.path.isfile(self.MaintLog):
|
|
try:
|
|
with open(self.MaintLog) as infile:
|
|
self.MaintLogList = json.load(infile)
|
|
return self.MaintLogList
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetMaintLogDict: " + str(e1))
|
|
return []
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetMaintLogDict (2): " + str(e1))
|
|
|
|
return []
|
|
|
|
# ---------- GeneratorController::UpdateMaintLog----------------------------
|
|
def SaveMaintLog(self, NewLog):
|
|
try:
|
|
self.MaintLogList = NewLog
|
|
with open(self.MaintLog, "w") as outfile:
|
|
json.dump(
|
|
self.MaintLogList, outfile, sort_keys=True, indent=4
|
|
) # , ensure_ascii = False)
|
|
outfile.flush()
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SaveMaintLog: " + str(e1))
|
|
return "Error in SaveMaintLog: " + str(e1)
|
|
|
|
# ---------- 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)
|
|
return "OK"
|
|
|
|
# ---------- GeneratorController::EditMaintLogRow---------------------------
|
|
def EditMaintLogRow(self, InputString):
|
|
|
|
# { index : {maint log entry}}
|
|
ValidInput = False
|
|
EntryString = InputString
|
|
if EntryString == None or not len(EntryString):
|
|
return "Invalid input for Edit Maintenance Log entry."
|
|
|
|
EntryString = EntryString.strip()
|
|
if EntryString.startswith("edit_row_maint_log"):
|
|
EntryString = EntryString[len("edit_row_maint_log") :]
|
|
EntryString = EntryString.strip()
|
|
if EntryString.strip().startswith("="):
|
|
EntryString = EntryString[len("=") :]
|
|
EntryString = EntryString.strip()
|
|
ValidInput = True
|
|
|
|
if ValidInput:
|
|
try:
|
|
EntryDict = json.loads(EntryString)
|
|
MaintLog = self.GetMaintLogDict()
|
|
for index, Entry in EntryDict.items():
|
|
# validate object
|
|
if not self.ValidateMaintLogEntry(Entry):
|
|
self.LogError(
|
|
"Error in EditMaintLogRow: failed validate entry in update"
|
|
)
|
|
return "Invalid edit maintenance log entry"
|
|
|
|
if not len(MaintLog):
|
|
self.LogError("Error in EditMaintLogRow: maint log is empty")
|
|
return "Error"
|
|
del MaintLog[int(index)]
|
|
# save log
|
|
MaintLog.insert(int(index), Entry)
|
|
self.SaveMaintLog(MaintLog)
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in EditMaintLogRow: " + str(e1))
|
|
return "Invalid input for Edit Maintenance Log entry (2)."
|
|
else:
|
|
self.LogError(
|
|
"Error in EditMaintLogRow: invalid input: " + str(InputString)
|
|
)
|
|
return "Invalid input for Edit Maintenance Log entry (3)."
|
|
return "OK"
|
|
|
|
# ---------- GeneratorController::DeleteMaintLogRow-------------------------
|
|
def DeleteMaintLogRow(self, command):
|
|
|
|
try:
|
|
CmdList = command.split("=")
|
|
if len(CmdList) == 2:
|
|
index = int(CmdList[1])
|
|
MaintLog = self.GetMaintLogDict()
|
|
if not len(MaintLog):
|
|
self.LogError("Error in DeleteMaintLogRow: maint log is empty")
|
|
return "Error"
|
|
|
|
del MaintLog[int(index)]
|
|
# save log
|
|
self.SaveMaintLog(MaintLog)
|
|
else:
|
|
self.LogError(
|
|
"Error in DeleteMaintLogRow: invalid input: " + str(CmdList)
|
|
)
|
|
return "Error"
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DeleteMaintLogRow: " + str(e1))
|
|
return "Error in DeleteMaintLogRow: " + str(e1)
|
|
return "OK"
|
|
|
|
|
|
# ---------- GeneratorController::Close-------------------------------------
|
|
def Close(self):
|
|
|
|
try:
|
|
# Controller
|
|
self.IsStopping = True
|
|
try:
|
|
self.InitCompleteEvent.set()
|
|
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("MaintenanceHouseKeepingThread")
|
|
except:
|
|
pass
|
|
try:
|
|
self.KillThread("PowerMeter")
|
|
except:
|
|
pass
|
|
try:
|
|
self.KillThread("SensorCollectionThread")
|
|
except:
|
|
pass
|
|
|
|
if self.ModBus != None:
|
|
# close modbus last
|
|
try:
|
|
if self.ControllerSelected.lower() == "h_100":
|
|
# H100 has problems with locking up if partial modbus packets are sent so delay here
|
|
time.sleep(0.5)
|
|
self.ModBus.Close()
|
|
except:
|
|
pass
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error Closing Controller: " + str(e1))
|
|
|
|
with self.CriticalLock:
|
|
self.InitComplete = False
|