mirror of
https://github.com/jgyates/genmon.git
synced 2026-08-12 15:32:00 -04:00
2581 lines
113 KiB
Python
2581 lines
113 KiB
Python
#!/usr/bin/env python
|
|
# -------------------------------------------------------------------------------
|
|
# FILE: custom_controller.py
|
|
# PURPOSE: Controller module for defining a custom generator controller
|
|
#
|
|
# AUTHOR: Jason G Yates
|
|
# DATE: 24-Jul-2021
|
|
#
|
|
# MODIFICATIONS:
|
|
# -------------------------------------------------------------------------------
|
|
|
|
import collections
|
|
import datetime
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import threading
|
|
import time
|
|
import copy
|
|
import struct
|
|
import math
|
|
|
|
from genmonlib.controller import GeneratorController
|
|
from genmonlib.modbus_file import ModbusFile
|
|
from genmonlib.mymodbus import ModbusProtocol
|
|
from genmonlib.mytile import MyTile
|
|
|
|
|
|
class CustomController(GeneratorController):
|
|
|
|
# ---------------------CustomController::__init__----------------------------
|
|
def __init__(
|
|
self,
|
|
log,
|
|
newinstall=False,
|
|
simulation=False,
|
|
simulationfile=None,
|
|
message=None,
|
|
feedback=None,
|
|
config=None,
|
|
):
|
|
|
|
# call parent constructor
|
|
super(CustomController, self).__init__(
|
|
log,
|
|
newinstall=newinstall,
|
|
simulation=simulation,
|
|
simulationfile=simulationfile,
|
|
message=message,
|
|
feedback=feedback,
|
|
config=config,
|
|
)
|
|
|
|
self.LastEngineState = ""
|
|
self.CurrentAlarmState = False
|
|
self.VoltageConfig = None
|
|
self.AlarmAccessLock = (
|
|
threading.RLock()
|
|
) # lock to synchronize access to the logs
|
|
self.EventAccessLock = (
|
|
threading.RLock()
|
|
) # lock to synchronize access to the logs
|
|
self.ConfigValidated = False
|
|
self.ControllerDetected = False
|
|
# for custom controllers
|
|
self.SerialBaudRate = 9600
|
|
self.SerialParity = None
|
|
self.SerialOnePointFiveStopBits = False
|
|
|
|
self.DaysOfWeek = {
|
|
0: "Sunday", # decode for register values with day of week
|
|
1: "Monday",
|
|
2: "Tuesday",
|
|
3: "Wednesday",
|
|
4: "Thursday",
|
|
5: "Friday",
|
|
6: "Saturday",
|
|
}
|
|
self.MonthsOfYear = {
|
|
1: "January", # decode for register values with month
|
|
2: "February",
|
|
3: "March",
|
|
4: "April",
|
|
5: "May",
|
|
6: "June",
|
|
7: "July",
|
|
8: "August",
|
|
9: "September",
|
|
10: "October",
|
|
11: "November",
|
|
12: "December",
|
|
}
|
|
|
|
self.SetupClass()
|
|
|
|
# -------------CustomController:SetupClass-----------------------------------
|
|
def SetupClass(self):
|
|
|
|
# read config file
|
|
try:
|
|
if not self.GetConfig():
|
|
self.FatalError("Failure in Controller GetConfig")
|
|
return None
|
|
except Exception as e1:
|
|
self.FatalError("Error reading config file: " + str(e1))
|
|
return None
|
|
|
|
try:
|
|
# Starting device connection
|
|
if self.Simulation:
|
|
self.ModBus = ModbusFile(
|
|
self.UpdateRegisterList,
|
|
inputfile=self.SimulationFile,
|
|
config=self.config,
|
|
)
|
|
else:
|
|
self.ModBus = ModbusProtocol(
|
|
self.UpdateRegisterList,
|
|
rate=self.SerialBaudRate,
|
|
Parity=self.SerialParity,
|
|
OnePointFiveStopBits=self.SerialOnePointFiveStopBits,
|
|
config=self.config,
|
|
)
|
|
|
|
self.ModBus.AlternateFileProtocol = self.AlternateFileProtocol
|
|
self.Threads = self.MergeDicts(self.Threads, self.ModBus.Threads)
|
|
self.LastRxPacketCount = self.ModBus.RxPacketCount
|
|
|
|
self.StartCommonThreads()
|
|
|
|
except Exception as e1:
|
|
self.FatalError("Error opening modbus device: " + str(e1))
|
|
return None
|
|
|
|
# ---------------------CustomController::GetConfig---------------------------
|
|
# read conf file, used internally, not called by genmon
|
|
# return True on success, else False
|
|
def GetConfig(self):
|
|
|
|
try:
|
|
|
|
self.AlternateFileProtocol = self.config.ReadValue(
|
|
"alternatefileprotocol", return_type=bool, default=True
|
|
)
|
|
self.VoltageConfig = self.config.ReadValue(
|
|
"voltageconfiguration", default="277/480"
|
|
)
|
|
self.NominalBatteryVolts = int(
|
|
self.config.ReadValue("nominalbattery", return_type=int, default=12)
|
|
)
|
|
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.UseFuelSensor = self.config.ReadValue(
|
|
"usesensorforfuelgauge", return_type=bool, default=True
|
|
)
|
|
self.UseCalculatedPower = self.config.ReadValue(
|
|
"usecalculatedpower", return_type=bool, default=False
|
|
)
|
|
# used for controllers that use serial comms other than 9600, N, 8, 1
|
|
# NOTE: This is only for custom controllers
|
|
if self.config.HasOption("serial_baud_rate"):
|
|
self.SerialBaudRate = self.config.ReadValue(
|
|
"serial_baud_rate", return_type=int, default=9600
|
|
)
|
|
# None, Even or Odd
|
|
self.SerialParity = self.config.ReadValue("serial_parity", default=None)
|
|
if self.SerialParity == None or self.SerialParity.lower() == "none" or self.SerialParity == "0":
|
|
self.SerialParity = None
|
|
elif self.SerialParity.lower() == "odd" or self.SerialParity == "1":
|
|
self.SerialParity = 1
|
|
else:
|
|
self.SerialParity = 2
|
|
if self.config.HasOption("serial_one_point_five_stop_bits"):
|
|
self.SerialOnePointFiveStopBits = self.config.ReadValue(
|
|
"serial_one_point_five_stop_bits", return_type=bool, default=False
|
|
)
|
|
|
|
self.ignore_alarms =[]
|
|
ignore_alarms = self.config.ReadValue("ignore_alarms", default = None)
|
|
|
|
if ignore_alarms != None:
|
|
if len(ignore_alarms):
|
|
AList = ignore_alarms.strip().split(",")
|
|
if len(AList):
|
|
self.ignore_alarms = []
|
|
for Items in AList:
|
|
self.ignore_alarms.append(Items.strip())
|
|
self.LogDebug("Ignored Alarms: " + str(self.ignore_alarms))
|
|
|
|
|
|
self.ConfigImportFile = self.config.ReadValue(
|
|
"import_config_file", default="Evolution_Liquid_Cooled.json"
|
|
)
|
|
if self.ConfigImportFile == None:
|
|
self.FatalError("Missing entry import_config_file. Unable to continue.")
|
|
|
|
self.ConfigFileName = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
|
|
"data",
|
|
"controller",
|
|
self.ConfigImportFile,
|
|
)
|
|
|
|
self.LogError("Using config import: " + self.ConfigFileName)
|
|
if not self.ReadImportConfig():
|
|
self.FatalError("Unable to read import config: " + self.ConfigFileName)
|
|
return False
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetConfig: " + str(e1))
|
|
self.FatalError(
|
|
"Missing config file or config file entries (CustomController): "
|
|
+ str(e1)
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
# -------------CustomController:ReadImportConfig-----------------------------
|
|
def ReadImportConfig(self):
|
|
|
|
if os.path.isfile(self.ConfigFileName):
|
|
try:
|
|
with open(self.ConfigFileName) as infile:
|
|
self.controllerimport = json.load(infile)
|
|
except Exception as e1:
|
|
self.LogErrorLine(
|
|
"Error in GetConfig reading config import file: " + str(e1)
|
|
)
|
|
return False
|
|
else:
|
|
self.LogError(
|
|
"Error reading config import file: " + str(self.ConfigFileName)
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
# -------------CustomController:GetSingleEntry-------------------------------
|
|
# get a single value from JSON config or a dynamic value from modbus
|
|
# Return either a modbus value or a single numeric value from JSON
|
|
def GetSingleEntry(self, entry_name, JSONNum=False):
|
|
try:
|
|
if not entry_name in self.controllerimport.keys():
|
|
return False, None
|
|
ImportedEntry = self.controllerimport[entry_name]
|
|
if isinstance(ImportedEntry, dict):
|
|
ImportedTitle, ImportedValue = self.GetDisplayEntry(
|
|
ImportedEntry, JSONNum=JSONNum, no_units=True
|
|
)
|
|
if ImportedValue == None:
|
|
return False, None
|
|
elif self.IsString(ImportedEntry):
|
|
return True, ImportedEntry
|
|
else:
|
|
ImportedValue = int(ImportedEntry)
|
|
return True, ImportedValue
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetSingleEntry: " + entry_name + ": " + str(e1))
|
|
return False, None
|
|
|
|
# -------------CustomController:IdentifyController---------------------------
|
|
def IdentifyController(self):
|
|
|
|
try:
|
|
if self.ControllerDetected:
|
|
return
|
|
if (self.Model != None and self.Model.lower() != str(self.controllerimport["controller_name"]).lower()) or self.Model == None:
|
|
self.config.WriteValue("model", str(self.controllerimport["controller_name"]))
|
|
|
|
self.Model = str(self.controllerimport["controller_name"])
|
|
|
|
ReturnValue = False
|
|
|
|
TempValue = None
|
|
ReturnValue, TempValue = self.GetSingleEntry("rated_nominal_voltage")
|
|
if not ReturnValue:
|
|
return False
|
|
if isinstance(TempValue, int):
|
|
if TempValue == 0:
|
|
TempValue = 480
|
|
self.NominalLineVolts = TempValue
|
|
elif self.StringIsInt(TempValue):
|
|
self.NominalLineVolts = int(TempValue)
|
|
elif type(TempValue) == str and TempValue.lower() == "unknown":
|
|
self.NominalLineVolts = self.NominalLineVolts
|
|
else:
|
|
self.LogDebug("Nominal Line Volts: " + str(self.NominalLineVolts))
|
|
|
|
ReturnValue, TempValue = self.GetSingleEntry("nominal_battery_voltage")
|
|
if not ReturnValue:
|
|
return False
|
|
if isinstance(TempValue, int):
|
|
if TempValue == 0:
|
|
TempValue = 24
|
|
self.NominalBatteryVolts = TempValue
|
|
elif self.StringIsInt(TempValue):
|
|
self.NominalBatteryVolts = int(TempValue)
|
|
elif type(TempValue) == str and TempValue.lower() == "unknown":
|
|
self.NominalBatteryVolts = self.NominalBatteryVolts # use etc config file value
|
|
else:
|
|
self.LogDebug("Nominal Battery Volts: " + str(self.NominalBatteryVolts))
|
|
|
|
ReturnValue, TempValue = self.GetSingleEntry("rated_nominal_freq")
|
|
if not ReturnValue:
|
|
return False
|
|
if isinstance(TempValue, int):
|
|
if TempValue == 0:
|
|
TempValue = 60
|
|
self.NominalFreq = TempValue
|
|
elif self.StringIsInt(TempValue):
|
|
self.NominalFreq = int(TempValue)
|
|
elif type(TempValue) == str and TempValue.lower() == "unknown":
|
|
if type(self.NominalFreq) == str and self.NominalFreq.lower() == "unknown":
|
|
self.NominalFreq = 60
|
|
else:
|
|
self.LogDebug("Nominal Frequency: " + str(self.NominalFreq))
|
|
else:
|
|
self.LogDebug("Nominal Frequency: " + str(self.NominalFreq))
|
|
|
|
ReturnValue, TempValue = self.GetSingleEntry("rated_nominal_rpm")
|
|
if not ReturnValue:
|
|
return False
|
|
|
|
if isinstance(TempValue, int):
|
|
if TempValue == 0:
|
|
TempValue = 1800
|
|
self.NominalRPM = TempValue
|
|
elif self.StringIsInt(TempValue):
|
|
self.NominalRPM = int(TempValue)
|
|
elif type(TempValue) == str and TempValue.lower() == "unknown":
|
|
if type(self.NominalRPM) == str and self.NominalRPM.lower() == "unknown":
|
|
self.NominalRPM = 3600 # default for genmon
|
|
else:
|
|
self.LogDebug("Nominal RPM: " + str(self.NominalRPM))
|
|
else:
|
|
self.LogDebug("Nominal RPM: " + str(self.NominalRPM))
|
|
|
|
ReturnValue, TempValue = self.GetSingleEntry("rated_max_output_power_kw")
|
|
if not ReturnValue:
|
|
return False
|
|
if isinstance(TempValue, int):
|
|
if TempValue == 0:
|
|
TempValue = 100
|
|
self.NominalKW = str(TempValue)
|
|
elif self.StringIsFloat(TempValue) or self.StringIsInt(TempValue):
|
|
self.NominalKW = str(TempValue)
|
|
else:
|
|
self.LogDebug("Nominal kW: " + str(self.NominalKW))
|
|
|
|
ReturnValue, TempValue = self.GetSingleEntry("generator_phase")
|
|
if not ReturnValue:
|
|
return False
|
|
if isinstance(TempValue, int):
|
|
self.Phase = TempValue
|
|
if self.Phase != 1:
|
|
self.Phase = 3
|
|
elif self.StringIsInt(TempValue):
|
|
self.Phase = int(TempValue)
|
|
else:
|
|
self.LogDebug("Phase: " + str(self.Phase))
|
|
|
|
if "identity" in self.controllerimport.keys():
|
|
Controller_Identity = self.GetExtendedDisplayString(self.controllerimport, "identity")
|
|
self.LogDebug("Controller ID: " + str(Controller_Identity))
|
|
if "unknown" in Controller_Identity.lower():
|
|
self.ControllerDetected = True
|
|
else:
|
|
self.ControllerDetected = True
|
|
else:
|
|
self.ControllerDetected = True
|
|
return self.ControllerDetected
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in IdentifyController: " + str(e1))
|
|
return False
|
|
|
|
# -------------CustomController:ValidateConfig---------------------------
|
|
def ValidateConfig(self):
|
|
|
|
try:
|
|
if self.ControllerDetected:
|
|
return True
|
|
|
|
# at this point we will parse the imported config from the JSON file
|
|
if not "switch_state" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain switch_state")
|
|
return False
|
|
if not "alarm_conditions" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain alarm_conditions")
|
|
return False
|
|
if not "engine_state" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain engine_state")
|
|
return False
|
|
# base registers are holding registers (Modbus Function 3), input registers are Modbus Function 4
|
|
if not "holding_registers" in self.controllerimport.keys() and not "input_registers" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain holding_registers and input_registers")
|
|
return False
|
|
if not "status" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain status")
|
|
return False
|
|
if not "maintenance" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain maintenance")
|
|
return False
|
|
if not "controller_name" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain controller_name")
|
|
return False
|
|
|
|
if not "rated_max_output_power_kw" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain rated_max_output_power_kw")
|
|
return False
|
|
if not "rated_nominal_voltage" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain rated_nominal_voltage")
|
|
return False
|
|
if not "rated_nominal_freq" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain rated_nominal_freq")
|
|
return False
|
|
if not "rated_nominal_rpm" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain rated_nominal_rpm")
|
|
return False
|
|
if not "generator_phase" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain generator_phase")
|
|
return False
|
|
if not "nominal_battery_voltage" in self.controllerimport.keys():
|
|
self.LogError("Error: Controller Import does not contain nominal_battery_voltage")
|
|
return False
|
|
|
|
if "holding_registers" in self.controllerimport.keys():
|
|
for Register, RegisterData in self.controllerimport["holding_registers"].items():
|
|
if Register.lower().startswith("comment"):
|
|
continue
|
|
if isinstance(RegisterData, dict):
|
|
Length = int(RegisterData["length"])
|
|
else:
|
|
Length = int(RegisterData)
|
|
if Length % 2 != 0:
|
|
self.LogError(
|
|
"Error: Controller Import: modbus register lenghts must be divisible by 2 (holding_registers): "
|
|
+ str(Register)
|
|
+ ":"
|
|
+ str(Length)
|
|
)
|
|
return False
|
|
if "input_registers" in self.controllerimport.keys():
|
|
for Register, RegisterData in self.controllerimport["input_registers"].items():
|
|
if Register.lower().startswith("comment"):
|
|
continue
|
|
if isinstance(RegisterData, dict):
|
|
Length = RegisterData["length"]
|
|
else:
|
|
Length = RegisterData
|
|
if Length % 2 != 0:
|
|
self.LogError(
|
|
"Error: Controller Import: modbus register lenghts must be divisible by 2 (input_registers): "
|
|
+ str(Register)
|
|
+ ":"
|
|
+ str(Length)
|
|
)
|
|
return False
|
|
|
|
if "coil_registers" in self.controllerimport.keys():
|
|
for Register, RegisterData in self.controllerimport["coil_registers"].items():
|
|
if Register.lower().startswith("comment"):
|
|
continue
|
|
if isinstance(RegisterData, dict):
|
|
Length = RegisterData["length"]
|
|
else:
|
|
Length = RegisterData
|
|
if Length != 1:
|
|
self.LogError(
|
|
"Error: Controller Import: modbus register lenghts must be 1 for coil registers"
|
|
+ str(Register)
|
|
+ ":"
|
|
+ str(Length)
|
|
)
|
|
return False
|
|
|
|
self.LogDebug("Configuration Validated!")
|
|
self.ConfigValidated = True
|
|
|
|
return True
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ValidateConfig: " + str(e1))
|
|
return False
|
|
|
|
# -------------CustomController:InitDevice-----------------------------------
|
|
# One time reads, and read all registers once
|
|
def InitDevice(self):
|
|
|
|
try:
|
|
self.ValidateConfig()
|
|
self.MasterEmulation()
|
|
self.IdentifyController()
|
|
self.SetupTiles()
|
|
self.InitComplete = True
|
|
self.InitCompleteEvent.set()
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in InitDevice: " + str(e1))
|
|
|
|
# -------------CustomController:SetupTiles-----------------------------------
|
|
def SetupTiles(self):
|
|
try:
|
|
with self.ExternalDataLock:
|
|
self.TileList = []
|
|
|
|
sensor_list = self.controllerimport["gauges"]
|
|
|
|
for sensor in sensor_list:
|
|
|
|
if "title" not in sensor:
|
|
self.LogError("Error in SetupTiles: no " "title" " in sensor entry")
|
|
continue
|
|
|
|
if "sensor" not in sensor:
|
|
self.LogError(
|
|
"Error in SetupTiles: sensor (" + sensor["title"] + ") has no ""sensor"" entry"
|
|
)
|
|
continue
|
|
if "units" not in sensor:
|
|
self.LogError(
|
|
"Error in SetupTiles: sensor (" + sensor["title"] + ") has no ""units"" entry"
|
|
)
|
|
continue
|
|
units = sensor["units"]
|
|
|
|
if "nominal" not in sensor:
|
|
self.LogError(
|
|
"Error in SetupTiles: sensor (" + sensor["title"] + ") has no ""nominal"" entry"
|
|
)
|
|
continue
|
|
|
|
# add inclulde object if needed
|
|
if "include" in sensor.keys() and "include" in self.controllerimport.keys():
|
|
includeNames = sensor["include"]
|
|
includeList = includeNames.split(",")
|
|
for includeName in includeList:
|
|
if includeName in self.controllerimport["include"].keys():
|
|
includeValue = self.controllerimport["include"][includeName]
|
|
if isinstance(includeValue, dict):
|
|
sensor = self.MergeDicts(sensor, includeValue)
|
|
else:
|
|
self.LogError("Error in SetupTiles: include value is not an object: " + str(includeValue))
|
|
continue
|
|
else:
|
|
self.LogError("Error in SetupTiles: entry has include but object name is not in scope of base include object: " + str(sensor))
|
|
continue
|
|
|
|
if "reg" not in sensor:
|
|
self.LogError("Error in SetupTiles: no " "reg" " in sensor entry")
|
|
continue
|
|
|
|
# check for "disable" object
|
|
if "disable" in sensor.keys():
|
|
# conditionally disable the entry
|
|
if "reg_type" in sensor.keys():
|
|
reg_type = sensor["reg_type"]
|
|
else:
|
|
reg_type = None # holding register
|
|
|
|
title, value = self.GetDisplayEntry(sensor["disable"], inheritreg = sensor["reg"], inheritregtype = reg_type)
|
|
if value:
|
|
#self.LogDebug("Ignoring gauge " + sensor["title"])
|
|
continue
|
|
|
|
if "maximum" in sensor.keys():
|
|
maximum = sensor["maximum"]
|
|
else:
|
|
maximum = None
|
|
if "values" in sensor:
|
|
values = sensor["values"]
|
|
else:
|
|
values = None
|
|
|
|
if not isinstance(sensor["nominal"], int) and sensor["nominal"].lower() == "unknown":
|
|
if sensor["sensor"].lower() == "power" or sensor["sensor"].lower() == "powergraph":
|
|
nominal = float(self.NominalKW)
|
|
elif sensor["sensor"].lower() == "linevolts":
|
|
nominal = self.NominalLineVolts
|
|
elif sensor["sensor"].lower() == "frequency":
|
|
try:
|
|
nominal = int(self.NominalFreq)
|
|
except:
|
|
nominal = 60
|
|
elif sensor["sensor"].lower() == "batteryvolts":
|
|
try:
|
|
nominal = self.NominalBatteryVolts
|
|
except:
|
|
nominal = 12
|
|
elif sensor["sensor"].lower() == "rpm":
|
|
try:
|
|
nominal = int(self.NominalRPM)
|
|
except:
|
|
nominal = 1800
|
|
elif sensor["sensor"].lower() == "current":
|
|
nominal = round((float(self.NominalKW) * 1000) / self.NominalLineVolts,0)
|
|
else:
|
|
nominal = None
|
|
self.LogError("Nominal is unknown for type " + sensor["sensor"])
|
|
else:
|
|
if sensor["sensor"].lower() == "temperature":
|
|
if "nominal" in sensor.keys():
|
|
nominal = self.ProcessTemperatureModifier(sensor, sensor["nominal"], units = False)
|
|
if "maximum" in sensor.keys():
|
|
maximum = self.ProcessTemperatureModifier(sensor, sensor["maximum"], units = False)
|
|
if "units" in sensor.keys():
|
|
units = self.ProcessTemperatureModifier(sensor, sensor["units"], units = True)
|
|
else:
|
|
nominal = sensor["nominal"]
|
|
|
|
if maximum == None:
|
|
maximum = round(nominal * 1.25, 0)
|
|
#self.LogDebug(f"SetupTiles: Max set to {sensor["sensor"].lower()}: {maximum}, nominal {nominal}")
|
|
#else:
|
|
#self.LogDebug(f"SetupTiles: not set Max to {sensor["sensor"].lower()}:{maximum}, nominal {nominal}")
|
|
|
|
Tile = MyTile(
|
|
self.log,
|
|
title=sensor["title"],
|
|
type=sensor["sensor"],
|
|
units=units,
|
|
nominal=nominal,
|
|
maximum=maximum,
|
|
values=values,
|
|
callback=self.GetGaugeValue,
|
|
callbackparameters=(sensor["title"],),
|
|
)
|
|
self.TileList.append(Tile)
|
|
|
|
self.SetupCommonTiles()
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetupTiles: " + str(e1))
|
|
|
|
# ------------ CustomController:WaitAndPergeforTimeout ----------------------
|
|
def WaitAndPergeforTimeout(self):
|
|
# if we get here a timeout occured, and we have recieved at least one good packet
|
|
# this logic is to keep from receiving a packet that we have already requested once we
|
|
# timeout and start to request another
|
|
# Wait for a bit to allow any missed response from the controller to arrive
|
|
# otherwise this could get us out of sync
|
|
# This assumes MasterEmulation is called from ProcessThread
|
|
if self.WaitForExit(
|
|
"ProcessThread", float(self.ModBus.ModBusPacketTimoutMS / 1000.0)
|
|
): #
|
|
return
|
|
self.ModBus.Flush()
|
|
|
|
# -------------CustomController:RegisterIsLog--------------------------------
|
|
def RegisterIsLog(self, Register):
|
|
try:
|
|
# return True for sucess, Register Length (in bytes), and text name
|
|
if not "log_registers" in self.controllerimport.keys():
|
|
return False, 0, ""
|
|
RegInt = int(Register,16)
|
|
for LogRegister, LogRegisterData in self.controllerimport["log_registers"].items():
|
|
if LogRegister.lower().startswith("comment"):
|
|
continue
|
|
LogRegInt = int(LogRegister,16)
|
|
LogRegEndOffset = int(LogRegisterData["step"]) * int(LogRegisterData["iteration"])
|
|
if RegInt >= LogRegInt and RegInt <= (LogRegInt + LogRegEndOffset):
|
|
return True, LogRegisterData["length"], LogRegisterData["text"]
|
|
return False, 0, ""
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in RegisterIsLog: " + str(e1))
|
|
return False, 0, ""
|
|
|
|
# -------------CustomController:UpdateLogRegistersAsMaster------------------
|
|
def UpdateLogRegistersAsMaster(self):
|
|
try:
|
|
if not "log_registers" in self.controllerimport.keys():
|
|
return
|
|
|
|
if not self.ConfigValidated:
|
|
self.ValidateConfig()
|
|
if not self.ConfigValidated:
|
|
return
|
|
for Register, RegisterData in self.controllerimport["log_registers"].items():
|
|
if Register.lower().startswith("comment"):
|
|
continue
|
|
if not isinstance(RegisterData, dict):
|
|
self.LogDebug("Invalid register data in log register description")
|
|
return
|
|
try:
|
|
IsInput = False
|
|
if "reg_type" in RegisterData.keys():
|
|
if RegisterData["reg_type"] == "input":
|
|
IsInput = True
|
|
Length = RegisterData["length"]
|
|
Step = RegisterData["step"]
|
|
Iteration = RegisterData["iteration"]
|
|
RegisterInt = int(Register, 16)
|
|
while(Iteration > 0):
|
|
Register = "%04x" % RegisterInt
|
|
if self.IsStopping:
|
|
return
|
|
localTimeoutCount = self.ModBus.ComTimoutError
|
|
localSyncError = self.ModBus.ComSyncError
|
|
self.ModBus.ProcessTransaction(Register, Length // 2, IsInput = IsInput)
|
|
if (
|
|
localSyncError != self.ModBus.ComSyncError
|
|
or localTimeoutCount != self.ModBus.ComTimoutError
|
|
) and self.ModBus.RxPacketCount:
|
|
self.WaitAndPergeforTimeout()
|
|
self.DelayBetweenFrames()
|
|
RegisterInt += Step
|
|
Iteration -= 1
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in MasterEmulation: " + str(e1))
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLing("Error in UpdateLogRegistersAsMaster: " + str(e1))
|
|
|
|
# -------------CustomController:MasterEmulation------------------------------
|
|
def MasterEmulation(self):
|
|
|
|
try:
|
|
if not self.ConfigValidated:
|
|
self.ValidateConfig()
|
|
if not self.ConfigValidated:
|
|
return
|
|
if "holding_registers" in self.controllerimport.keys():
|
|
for Register, RegisterData in self.controllerimport["holding_registers"].items():
|
|
if Register.lower().startswith("comment"):
|
|
continue
|
|
if isinstance(RegisterData, dict):
|
|
Length = RegisterData["length"]
|
|
else:
|
|
Length = RegisterData
|
|
try:
|
|
if self.IsStopping:
|
|
return
|
|
localTimeoutCount = self.ModBus.ComTimoutError
|
|
localSyncError = self.ModBus.ComSyncError
|
|
self.ModBus.ProcessTransaction(Register, Length // 2)
|
|
if (
|
|
localSyncError != self.ModBus.ComSyncError
|
|
or localTimeoutCount != self.ModBus.ComTimoutError
|
|
) and self.ModBus.RxPacketCount:
|
|
self.WaitAndPergeforTimeout()
|
|
self.DelayBetweenFrames()
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in MasterEmulation (holding): " + str(e1))
|
|
|
|
if "input_registers" in self.controllerimport.keys():
|
|
for Register, RegisterData in self.controllerimport["input_registers"].items():
|
|
if Register.lower().startswith("comment"):
|
|
continue
|
|
if isinstance(RegisterData, dict):
|
|
Length = RegisterData["length"]
|
|
else:
|
|
Length = RegisterData
|
|
try:
|
|
if self.IsStopping:
|
|
return
|
|
localTimeoutCount = self.ModBus.ComTimoutError
|
|
localSyncError = self.ModBus.ComSyncError
|
|
self.ModBus.ProcessTransaction(Register, Length // 2, IsInput = True)
|
|
if (
|
|
localSyncError != self.ModBus.ComSyncError
|
|
or localTimeoutCount != self.ModBus.ComTimoutError
|
|
) and self.ModBus.RxPacketCount:
|
|
self.WaitAndPergeforTimeout()
|
|
self.DelayBetweenFrames()
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in MasterEmulation (input): " + str(e1))
|
|
|
|
if "coil_registers" in self.controllerimport.keys():
|
|
for Register, RegisterData in self.controllerimport["coil_registers"].items():
|
|
if Register.lower().startswith("comment"):
|
|
continue
|
|
if isinstance(RegisterData, dict):
|
|
Length = RegisterData["length"]
|
|
else:
|
|
Length = RegisterData
|
|
try:
|
|
if self.IsStopping:
|
|
return
|
|
localTimeoutCount = self.ModBus.ComTimoutError
|
|
localSyncError = self.ModBus.ComSyncError
|
|
# force coil to have a length of 1
|
|
self.ModBus.ProcessTransaction(Register, 1, IsCoil = True)
|
|
if (
|
|
localSyncError != self.ModBus.ComSyncError
|
|
or localTimeoutCount != self.ModBus.ComTimoutError
|
|
) and self.ModBus.RxPacketCount:
|
|
self.WaitAndPergeforTimeout()
|
|
self.DelayBetweenFrames()
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in MasterEmulation (coil): " + str(e1))
|
|
|
|
if self.ControllerDetected == False:
|
|
self.IdentifyController()
|
|
self.CheckForAlarmEvent.set()
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in MasterEmulation: " + str(e1))
|
|
|
|
# ------------ CustomController:GetTransferStatus ---------------------------
|
|
def GetTransferStatus(self):
|
|
|
|
LineState = "Unknown"
|
|
# TODO
|
|
|
|
return LineState
|
|
|
|
# ------------ Evolution:CheckForOutage -------------------------------------
|
|
# also update min and max utility voltage
|
|
def CheckForOutage(self):
|
|
|
|
try:
|
|
if not self.InitComplete:
|
|
return
|
|
|
|
if not self.OutageSupported():
|
|
return
|
|
|
|
# get utility voltage, threshold voltage and pickup voltage
|
|
ReturnValue, UtilityVolts = self.GetSingleEntry("linevoltage")
|
|
if not ReturnValue or UtilityVolts == None:
|
|
return
|
|
ReturnValue, ThresholdVoltage = self.GetSingleEntry("thresholdvoltage")
|
|
if not ReturnValue or ThresholdVoltage == 0 or ThresholdVoltage == None:
|
|
ThresholdVoltage = int(self.NominalLineVolts * 0.60)
|
|
|
|
ReturnValue, PickupVoltage = self.GetSingleEntry("pickupvoltage")
|
|
if not ReturnValue or PickupVoltage == 0 or PickupVoltage == None:
|
|
PickupVoltage = int(self.NominalLineVolts * 0.80)
|
|
|
|
ThresholdVoltage = int(ThresholdVoltage)
|
|
PickupVoltage = int(PickupVoltage)
|
|
|
|
self.CheckForOutageCommon(UtilityVolts, ThresholdVoltage, PickupVoltage)
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in CheckForOutage: " + str(e1))
|
|
|
|
# ------------ CustomController:CheckForAlarms ------------------------------
|
|
def CheckForAlarms(self):
|
|
|
|
try:
|
|
status_included = False
|
|
if not self.InitComplete:
|
|
return
|
|
|
|
if self.OutageSupported():
|
|
self.CheckForOutage()
|
|
|
|
# Check for changes in engine state
|
|
EngineState = self.GetEngineState()
|
|
EngineState += self.GetSwitchState()
|
|
msgbody = ""
|
|
|
|
if len(self.UserURL):
|
|
msgbody += "For additional information : " + self.UserURL + "\n"
|
|
if not EngineState == self.LastEngineState:
|
|
self.LastEngineState = EngineState
|
|
|
|
self.UpdateLogRegistersAsMaster()
|
|
|
|
msgsubject = "Generator Notice: " + self.SiteName
|
|
if not self.SystemInAlarm():
|
|
msgbody += "NOTE: This message is a notice that the state of the generator has changed. The system is not in alarm.\n"
|
|
MessageType = "info"
|
|
else:
|
|
MessageType = "warn"
|
|
msgbody += self.GetMessageText()
|
|
status_included = True
|
|
msgbody += "\nIP Address: " + self.GetNetworkIp()
|
|
self.MessagePipe.SendMessage(msgsubject, msgbody, msgtype=MessageType)
|
|
|
|
# Check for Alarms
|
|
if self.SystemInAlarm():
|
|
if not self.CurrentAlarmState:
|
|
msgsubject = "Generator Notice: ALARM Active at " + self.SiteName
|
|
if not status_included:
|
|
msgbody += self.GetMessageText()
|
|
msgbody += "\nIP Address: " + self.GetNetworkIp()
|
|
self.MessagePipe.SendMessage(msgsubject, msgbody, msgtype="warn")
|
|
else:
|
|
if self.CurrentAlarmState:
|
|
msgsubject = "Generator Notice: ALARM Clear at " + self.SiteName
|
|
if not status_included:
|
|
msgbody += self.GetMessageText()
|
|
msgbody += "\nIP Address: " + self.GetNetworkIp()
|
|
self.MessagePipe.SendMessage(msgsubject, msgbody, msgtype="warn")
|
|
|
|
self.CurrentAlarmState = self.SystemInAlarm()
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in CheckForAlarms: " + str(e1))
|
|
|
|
return
|
|
|
|
# ------------ CustomController:UpdateRegisterList --------------------------
|
|
def UpdateRegisterList(self, Register, Value, IsString=False, IsFile=False, IsCoil = False, IsInput = False):
|
|
|
|
try:
|
|
if len(Register) != 4:
|
|
self.LogError(
|
|
"Validation Error: Invalid register value in UpdateRegisterList: %s %s"
|
|
% (Register, Value)
|
|
)
|
|
return False
|
|
|
|
if IsFile:
|
|
# todo validate file data length
|
|
self.FileData[Register] = Value
|
|
elif IsCoil:
|
|
self.Coils[Register] = Value
|
|
elif IsInput:
|
|
if self.ValidateRegister("input_registers", Register, Value):
|
|
self.Inputs[Register] = Value
|
|
else:
|
|
ReturnStatus, LogRegLength, Name = self.RegisterIsLog(Register)
|
|
if ReturnStatus:
|
|
self.Inputs[Register] = Value
|
|
else:
|
|
self.LogError("Failure validating log input register: " + Register)
|
|
return False
|
|
else: # base register (holding register)
|
|
# validate data length
|
|
if self.ValidateRegister("holding_registers", Register, Value):
|
|
self.Holding[Register] = Value
|
|
else:
|
|
ReturnStatus, LogRegLength, Name = self.RegisterIsLog(Register)
|
|
if ReturnStatus:
|
|
self.Holding[Register] = Value
|
|
else:
|
|
self.LogError("Failure validating log holding register: " + Register)
|
|
return False
|
|
|
|
return True
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in UpdateRegisterList: " + str(e1))
|
|
return False
|
|
|
|
# ---------- CustomController::ValidateRegister----------------------------
|
|
def ValidateRegister(self, type, Register, Value):
|
|
|
|
try:
|
|
datalength = int(len(Value) // 2)
|
|
if not type in self.controllerimport.keys():
|
|
self.LogError("Error in ValidateRegister: register " + str(Register) + ", type " + str(type) + " not in import data.")
|
|
return False
|
|
|
|
if Register in self.controllerimport[type]:
|
|
RegisterData = self.controllerimport[type][Register]
|
|
|
|
if isinstance(RegisterData, dict):
|
|
Length = RegisterData["length"]
|
|
else:
|
|
Length = RegisterData
|
|
if Length != datalength:
|
|
self.LogError("Invalid length detected in received modbus register " + str(Register) +
|
|
" : " + str(datalength) + ": " + str(Length) + ": [" + self.HexStringToString(Value) +"]")
|
|
return False
|
|
return True
|
|
else:
|
|
self.LogDebug("Error in ValidateRegister: register " + str(Register) + " not in " + str(type))
|
|
return False
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ValidateRegister: " + str(e1))
|
|
return False
|
|
|
|
# ---------- CustomController::GetRegisterLabels---------------------------
|
|
def GetRegisterLabels(self):
|
|
# return JSON of dict with registers and text descriptions
|
|
try:
|
|
ReturnDict = {}
|
|
#
|
|
if "holding_registers" in self.controllerimport.keys():
|
|
HoldingRegLabels = {}
|
|
for Register in self.Holding.keys():
|
|
if Register in self.controllerimport["holding_registers"].keys():
|
|
RegData = self.controllerimport["holding_registers"][Register]
|
|
if isinstance(RegData, dict):
|
|
HoldingRegLabels[Register] = RegData["text"]
|
|
else:
|
|
Success, Length, Name = self.RegisterIsLog(Register)
|
|
if Success:
|
|
HoldingRegLabels[Register] = Name
|
|
if len(HoldingRegLabels.keys()) > 0:
|
|
ReturnDict["Holding"] = HoldingRegLabels
|
|
if "input_registers" in self.controllerimport.keys():
|
|
InputRegLabels = {}
|
|
for Register in self.Inputs.keys():
|
|
if Register in self.controllerimport["input_registers"].keys():
|
|
RegData = self.controllerimport["input_registers"][Register]
|
|
if isinstance(RegData, dict):
|
|
InputRegLabels[Register] = RegData["text"]
|
|
else:
|
|
Success, Length, Name = self.RegisterIsLog(Register)
|
|
if Success:
|
|
InputRegLabels[Register] = Name
|
|
if len(InputRegLabels.keys()) > 0:
|
|
ReturnDict["Inputs"] = InputRegLabels
|
|
if "coil_registers" in self.controllerimport.keys():
|
|
CoilRegLabels = {}
|
|
for Register in self.Coils.keys():
|
|
if Register in self.controllerimport["coil_registers"].keys():
|
|
RegData = self.controllerimport["coil_registers"][Register]
|
|
if isinstance(RegData, dict):
|
|
CoilRegLabels[Register] = RegData["text"]
|
|
if len(CoilRegLabels.keys()) > 0:
|
|
ReturnDict["Coils"] = CoilRegLabels
|
|
return json.dumps(ReturnDict)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetRegisterLabels: " + str(e1))
|
|
return "{}"
|
|
|
|
# ---------------------CustomController::SystemInAlarm-----------------------
|
|
# return True if generator is in alarm, else False
|
|
def SystemInAlarm(self):
|
|
|
|
try:
|
|
if not "alarm_active" in self.controllerimport.keys():
|
|
alarm_list = self.GetExtendedDisplayString(self.controllerimport, "alarm_conditions", ReturnList=True)
|
|
if len(self.ignore_alarms):
|
|
alarm_list = list(filter(lambda x: x not in self.ignore_alarms, alarm_list))
|
|
if len(alarm_list) == 0:
|
|
return False
|
|
return True
|
|
alarm_state = self.GetExtendedDisplayString(self.controllerimport, "alarm_active")
|
|
if len(alarm_state) and not alarm_state == "Unknown":
|
|
return True
|
|
return False
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SystemInAlarm: " + str(e1))
|
|
return False
|
|
|
|
# ------------ CustomController:GetSwitchState ------------------------------
|
|
def GetSwitchState(self):
|
|
|
|
try:
|
|
return self.GetExtendedDisplayString(self.controllerimport, "switch_state")
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetSwitchState: " + str(e1))
|
|
return "Unknown"
|
|
|
|
# ------------ CustomController:GetGeneratorStatus --------------------------
|
|
def GetGeneratorStatus(self):
|
|
|
|
try:
|
|
if not "generator_status" in self.controllerimport.keys():
|
|
return "Unknown"
|
|
generator_status = self.GetExtendedDisplayString(self.controllerimport, "generator_status")
|
|
return generator_status
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetGeneratorStatus: " + str(e1))
|
|
return "Unknown"
|
|
|
|
# ------------ CustomController:GetEngineState ------------------------------
|
|
def GetEngineState(self):
|
|
|
|
try:
|
|
return self.GetExtendedDisplayString(self.controllerimport, "engine_state")
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetEngineState: " + str(e1))
|
|
return "Unknown"
|
|
|
|
# ------------ CustomController:GetDateTime ----------------------------------------
|
|
def GetDateTime(self):
|
|
|
|
ErrorReturn = "Unknown"
|
|
try:
|
|
# TODO
|
|
return ErrorReturn
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetDateTime: " + str(e1))
|
|
return ErrorReturn
|
|
|
|
# ------------ CustomController::OutageSupported -----------------------------------
|
|
def OutageSupported(self):
|
|
|
|
if self.DisableOutageCheck:
|
|
# do not check for outage
|
|
return False
|
|
|
|
if "linevoltage" in self.controllerimport.keys():
|
|
if "thresholdvoltage" in self.controllerimport.keys():
|
|
if "pickupvoltage" in self.controllerimport.keys():
|
|
return True
|
|
|
|
return False
|
|
|
|
# ------------ CustomController::GetStartInfo --------------------------------------
|
|
# return a dictionary with startup info for the gui
|
|
def GetStartInfo(self, NoTile=False):
|
|
|
|
try:
|
|
StartInfo = {}
|
|
|
|
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["PowerGraph"] = self.PowerMeterIsSupported()
|
|
StartInfo["Sensors"] = self.GetSensorNames()
|
|
StartInfo["NominalBatteryVolts"] = self.NominalBatteryVolts
|
|
StartInfo["FuelCalculation"] = self.FuelTankCalculationSupported()
|
|
StartInfo["FuelSensor"] = self.FuelSensorSupported()
|
|
StartInfo["FuelConsumption"] = self.FuelConsumptionSupported()
|
|
StartInfo["Controller"] = self.GetController(Actual=False)
|
|
StartInfo["Actual"] = self.GetController(Actual=True)
|
|
StartInfo["UtilityVoltage"] = False
|
|
StartInfo["RemoteCommands"] = False # Remote Start/ Stop/ StartTransfer
|
|
StartInfo["ResetAlarms"] = False
|
|
StartInfo["AckAlarms"] = False
|
|
StartInfo["RemoteTransfer"] = False # Remote start and transfer command
|
|
StartInfo["RemoteButtons"] = False # Remote controll of Off/Auto/Manual
|
|
StartInfo["ExerciseControls"] = False # self.SmartSwitch
|
|
StartInfo["WriteQuietMode"] = False
|
|
StartInfo["SetGenTime"] = ("settime" in self.controllerimport.keys())
|
|
StartInfo["import_config_file"] = self.ConfigImportFile
|
|
|
|
if self.Platform != None:
|
|
StartInfo["Linux"] = self.Platform.IsOSLinux()
|
|
StartInfo["RaspberryPi"] = self.Platform.IsPlatformRaspberryPi()
|
|
StartInfo["Platform"] = self.Platform.PlatformName()
|
|
|
|
if not NoTile:
|
|
|
|
StartInfo["buttons"] = self.GetButtons()
|
|
|
|
ShowStatus = "status" in self.controllerimport.keys()
|
|
ShowMaintenance = "maintenance" in self.controllerimport.keys()
|
|
ShowLogs = "logs" in self.controllerimport.keys()
|
|
|
|
StartInfo["pages"] = {
|
|
"status": ShowStatus,
|
|
"maint": ShowMaintenance,
|
|
"outage": self.OutageSupported(),
|
|
"logs": ShowLogs,
|
|
"monitor": True,
|
|
"maintlog": True,
|
|
"notifications": True,
|
|
"settings": True,
|
|
"addons": True,
|
|
"about": True,
|
|
}
|
|
|
|
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
|
|
|
|
# ------------ CustomController::GetStatusForGUI -----------------------------------
|
|
# return dict for GUI
|
|
def GetStatusForGUI(self):
|
|
|
|
try:
|
|
Status = {}
|
|
|
|
Status["basestatus"] = self.GetBaseStatus()
|
|
Status["switchstate"] = self.GetSwitchState()
|
|
Status["enginestate"] = self.GetEngineState()
|
|
Status["kwOutput"] = self.GetPowerOutput()
|
|
# TODO
|
|
value = self.GetVoltageOutput()
|
|
if value != None:
|
|
Status["OutputVoltage"] = value
|
|
value = self.GetBatteryVoltage()
|
|
if value != None:
|
|
Status["BatteryVoltage"] = value
|
|
value = self.GetUtilityVoltage()
|
|
if value != None:
|
|
Status["UtilityVoltage"] = value
|
|
value = self.GetFrequency()
|
|
if value != None:
|
|
Status["Frequency"] = value
|
|
value = self.GetRPM()
|
|
if value != None:
|
|
Status["RPM"] = value
|
|
|
|
# Exercise Info is a dict containing the following:
|
|
# Not supported
|
|
ExerciseInfo = collections.OrderedDict()
|
|
ExerciseInfo["Enabled"] = False
|
|
ExerciseInfo["Frequency"] = "Weekly" # Biweekly, Weekly or Monthly
|
|
ExerciseInfo["Hour"] = "14"
|
|
ExerciseInfo["Minute"] = "00"
|
|
ExerciseInfo["QuietMode"] = "Off"
|
|
ExerciseInfo["EnhancedExerciseMode"] = False
|
|
ExerciseInfo["Day"] = "Monday"
|
|
Status["ExerciseInfo"] = ExerciseInfo
|
|
|
|
Status["tiles"] = []
|
|
for Tile in self.TileList:
|
|
Status["tiles"].append(Tile.GetGUIInfo())
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetStatusForGUI: " + str(e1))
|
|
|
|
return Status
|
|
|
|
# ---------------------CustomController::DisplayLogs-------------------------
|
|
def DisplayLogs(self, AllLogs=False, DictOut=False, RawOutput=False):
|
|
|
|
# if DictOut is True, return a dictionary containing a Dictionaries (dict entry for each log)
|
|
# Each dict item a log (alarm, start/stop). For Example:
|
|
#
|
|
# Dict[Logs] = {"Alarm Log" : [Log Entry1, LogEntry2, ...]},
|
|
# {"Start Stop Log" : [Log Entry3, Log Entry 4, ...]}...
|
|
|
|
Logs = collections.OrderedDict()
|
|
LogDict = collections.OrderedDict()
|
|
Logs["Logs"] = LogDict
|
|
try:
|
|
if not "logs" in self.controllerimport.keys():
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Logs, ""))
|
|
|
|
for logitems in self.controllerimport["logs"]:
|
|
title, value = self.GetDisplayEntry(logitems)
|
|
if value != None:
|
|
LogDict[title] = value
|
|
else:
|
|
break
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayLogs: " + str(e1))
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Logs, ""))
|
|
return Logs
|
|
|
|
# ------------ CustomController::DisplayMaintenance -------------------------
|
|
def DisplayMaintenance(self, DictOut=False, JSONNum=False):
|
|
|
|
try:
|
|
# use ordered dict to maintain order of output
|
|
# ordered dict to handle evo vs nexus functions
|
|
Maintenance = collections.OrderedDict()
|
|
Maintenance["Maintenance"] = []
|
|
|
|
if not self.ControllerDetected or not self.InitComplete:
|
|
Maintenance["Maintenance"].append({"Genmon State": "Waiting for comms"})
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Maintenance, ""))
|
|
return Maintenance
|
|
|
|
Maintenance["Maintenance"].append({"Model": self.Model})
|
|
if "maintenance_due" in self.controllerimport.keys():
|
|
ServiceStr = self.GetExtendedDisplayString(self.controllerimport, "maintenance_due")
|
|
if ServiceStr == "Unknown" or ServiceStr == "" or ServiceStr == None:
|
|
Maintenance["Maintenance"].append({"Maintenance Due": "No"})
|
|
else:
|
|
Maintenance["Maintenance"].append({"Maintenance Due": "Yes"})
|
|
|
|
Maintenance["Maintenance"].append(
|
|
{"Controller Detected": self.GetController()}
|
|
)
|
|
Maintenance["Maintenance"].append({"Nominal RPM": self.NominalRPM})
|
|
Maintenance["Maintenance"].append({"Rated kW": self.NominalKW})
|
|
Maintenance["Maintenance"].append({"Nominal Frequency": self.NominalFreq})
|
|
Maintenance["Maintenance"].append({"Phase": self.Phase})
|
|
Maintenance["Maintenance"].append({"Fuel Type": self.FuelType})
|
|
|
|
Maintenance = self.DisplayMaintenanceCommon(Maintenance, JSONNum=JSONNum)
|
|
|
|
Maintenance["Maintenance"].extend(
|
|
self.GetDisplayList(self.controllerimport, "maintenance", JSONNum=JSONNum)
|
|
)
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayMaintenance: " + str(e1))
|
|
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Maintenance, ""))
|
|
|
|
return Maintenance
|
|
|
|
# ------------ CustomController::DisplayStatus ------------------------------
|
|
def DisplayStatus(self, DictOut=False, JSONNum=False):
|
|
|
|
try:
|
|
|
|
Status = collections.OrderedDict()
|
|
Status["Status"] = []
|
|
|
|
if not self.ControllerDetected or not self.InitComplete:
|
|
Status["Status"].append({"Genmon State": "Waiting for comms"})
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Status, ""))
|
|
return Status
|
|
|
|
gen_status = self.GetSwitchState()
|
|
if gen_status != "Unknown":
|
|
Status["Status"].append({"Switch State": gen_status})
|
|
else:
|
|
self.LogDebug("Switch State: " + gen_status)
|
|
|
|
gen_status = self.GetEngineState()
|
|
if gen_status != "Unknown":
|
|
Status["Status"].append({"Engine State": gen_status})
|
|
|
|
gen_status = self.GetGeneratorStatus()
|
|
if gen_status != "Unknown":
|
|
Status["Status"].append({"Generator Status": gen_status})
|
|
|
|
Status["Status"].extend(
|
|
self.GetDisplayList(self.controllerimport, "status", JSONNum=JSONNum)
|
|
)
|
|
|
|
if self.SystemInAlarm():
|
|
Status["Status"].append({"Alarm State": "System In Alarm"})
|
|
#activealarms = self.GetDisplayList(self.controllerimport, "alarm_conditions")
|
|
#Status["Status"].append(activealarms[0]
|
|
alarm_list = self.GetExtendedDisplayString(self.controllerimport, "alarm_conditions", ReturnList=True)
|
|
if len(self.ignore_alarms):
|
|
alarm_list = list(filter(lambda x: x not in self.ignore_alarms, alarm_list))
|
|
Status["Status"].append(
|
|
{
|
|
|
|
"Active Alarms": alarm_list
|
|
}
|
|
)
|
|
|
|
Status = self.DisplayStatusCommon(Status, JSONNum=JSONNum)
|
|
|
|
# Generator time
|
|
Time = []
|
|
Status["Status"].append({"Time": Time})
|
|
Time.append({"Monitor Time": datetime.datetime.now().strftime("%A %B %d, %Y %H:%M:%S")})
|
|
if "datetime" in self.controllerimport.keys():
|
|
retval, gentime = self.GetSingleEntry("datetime")
|
|
if retval or len(gentime) == 0:
|
|
Time.append({"Generator Time": gentime})
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayStatus: " + str(e1))
|
|
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Status, ""))
|
|
|
|
return Status
|
|
|
|
# ---------- GeneratorController::SetGeneratorTimeDate----------------------
|
|
# set generator time to system time
|
|
def SetGeneratorTimeDate(self):
|
|
|
|
try:
|
|
if not "settime" in self.controllerimport.keys():
|
|
return "Not Supported"
|
|
|
|
settime = self.controllerimport["settime"]
|
|
if not "command_sequence" in settime:
|
|
self.LogError("Erroir in SetGeneratorTimeDate: settime does not have a command_sequence element: " + str(settime))
|
|
return "Not Supported"
|
|
|
|
if not isinstance(settime["command_sequence"], list):
|
|
self.LogError("Error in SetGeneratorTimeDate: command_sequende is not a list: "+ str(settime))
|
|
return "Not Supported"
|
|
|
|
# get system time
|
|
d = datetime.datetime.now()
|
|
|
|
# attempt to make the seconds zero when we set the generator time so it will
|
|
# be very close to the system time
|
|
# Testing has show that this is not really achieving the seconds synced up, but
|
|
# it does make the time offset consistant
|
|
while d.second != 0:
|
|
time.sleep(60 - d.second) # sleep until seconds are zero
|
|
d = datetime.datetime.now()
|
|
|
|
CommandError = False
|
|
# make a copy of the command sequence so we can add "value" members to it
|
|
settime_copy = copy.deepcopy(settime)
|
|
for command in settime_copy["command_sequence"]:
|
|
hour = minute = day = month = year = 0
|
|
length = 2
|
|
if "length" in command.keys():
|
|
if(int(command["length"]) % 2 != 0):
|
|
self.LogDebug("Error in SetGeneratorTimeDate: Length is not a multiple of 2: " + str(settime))
|
|
length = 2
|
|
else:
|
|
length = int(command["length"])
|
|
if not "reg" in command.keys() or not isinstance(command["reg"], str):
|
|
self.LogError("Error in SetGeneratorTimeDate: invalid command string defined validateing reg: "+ str(settime))
|
|
CommandError = True
|
|
break
|
|
if "year" in command.keys() and isinstance(command["year"], str):
|
|
year = self.ProcessExecModifier(command, d.year, altname = "year")
|
|
if "month" in command.keys() and isinstance(command["month"], str):
|
|
month = self.ProcessExecModifier(command, d.month, altname = "month")
|
|
if "day" in command.keys() and isinstance(command["day"], str):
|
|
day = self.ProcessExecModifier(command, d.day, altname = "day")
|
|
if "hour" in command.keys() and isinstance(command["hour"], str):
|
|
hour = self.ProcessExecModifier(command, d.hour, altname = "hour")
|
|
if "minute" in command.keys() and isinstance(command["minute"], str):
|
|
minute = self.ProcessExecModifier(command, d.minute, altname = "minute")
|
|
|
|
value = hour | minute | day | month | year
|
|
# add the value to be written to the command in the command sequence list
|
|
command["value"] = value
|
|
|
|
if CommandError:
|
|
return "Error setting time"
|
|
# not that all the commands have been created we can execute the comand sequenc
|
|
with self.ModBus.CommAccessLock:
|
|
return self.ExecuteCommandSequence(settime_copy["command_sequence"])
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetGeneratorTimeDate: " + str(e1))
|
|
|
|
return "Invalid Return"
|
|
|
|
# ------------ CustomController:GetSingleSensor -----------------------------
|
|
def GetSingleSensor(self, dict_name, ReturnFloat=False, ReturnInt=False):
|
|
|
|
try:
|
|
if ReturnInt:
|
|
ReturnValue = 0
|
|
elif ReturnFloat:
|
|
ReturnValue = 0.0
|
|
else:
|
|
ReturnValue = ""
|
|
dict_results = self.controllerimport.get(dict_name, None)
|
|
|
|
if dict_results == None:
|
|
return ReturnValue
|
|
|
|
if ReturnInt or ReturnFloat:
|
|
no_units = True
|
|
else:
|
|
no_units = False
|
|
out_string = self.GetExtendedDisplayString(self.controllerimport, dict_name)
|
|
|
|
if not len(out_string):
|
|
return ReturnValue
|
|
if ReturnInt or ReturnFloat:
|
|
out_string = self.removeAlpha(out_string)
|
|
if ReturnInt:
|
|
if self.StringIsFloat(out_string):
|
|
return int(float(out_string))
|
|
return int(out_string)
|
|
elif ReturnFloat:
|
|
if self.StringIsInt(out_string):
|
|
return float(int(out_string))
|
|
return float(out_string)
|
|
else:
|
|
return out_string
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine(
|
|
"Error in GetSingleSensor: " + str(dict_name) + " : " + str(e1)
|
|
)
|
|
return "Unknown"
|
|
|
|
# ------------ GeneratorController:GetExtendedDisplayString -----------------
|
|
# returns one or multiple status strings
|
|
def GetExtendedDisplayString(self, inputdict, key_name, no_units=False, ReturnList = False, FirstItemOnly = False):
|
|
|
|
try:
|
|
# this returns a list of dicts
|
|
StateList = self.GetDisplayList(inputdict, key_name, no_units=no_units)
|
|
ListValues = []
|
|
# convert the list of dicts to just a list of values
|
|
for entry in StateList:
|
|
# entry.values() is a list of list if it was created from iterable objects
|
|
for key, values in entry.items():
|
|
if isinstance(values, list):
|
|
ListValues.extend(values)
|
|
else:
|
|
ListValues.append(values)
|
|
if ReturnList:
|
|
return ListValues
|
|
if FirstItemOnly and len(ListValues):
|
|
return ListValues[0]
|
|
ReturnString = ", ".join(ListValues)
|
|
if not len(ReturnString):
|
|
return "Unknown"
|
|
return ReturnString
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetExtendedDisplayString: " + str(e1))
|
|
return "Unknown"
|
|
|
|
# ------------ GeneratorController:GetGaugeValue ----------------------------
|
|
def GetGaugeValue(self, sensor_title):
|
|
|
|
try:
|
|
sensor_list = self.GetDisplayList(
|
|
self.controllerimport, "gauges", no_units=True
|
|
)
|
|
|
|
for sensor in sensor_list:
|
|
if sensor_title in list(sensor.keys()):
|
|
items = list(sensor.values())
|
|
if len(items) == 1:
|
|
return items[0]
|
|
return None
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetGaugeValue: " + str(e1))
|
|
return None
|
|
|
|
# ------------ GeneratorController:GetDisplayList ---------------------------
|
|
# parse a list of modbus values (expressed as dicts) and any sub lists of
|
|
# values (also expressed as dicts, return a displayable dict with parsed values
|
|
def GetDisplayList(self, inputdict, key_name, JSONNum=False, no_units=False):
|
|
|
|
ReturnValue = []
|
|
try:
|
|
default = None
|
|
ParseList = inputdict.get(key_name, None)
|
|
if not isinstance(ParseList, list) or ParseList == None:
|
|
self.LogDebug("Error in GetDisplayList: invalid input or data: " + str(key_name))
|
|
return ReturnValue
|
|
|
|
for Entry in ParseList:
|
|
if not isinstance(Entry, dict):
|
|
self.LogError( "Error in GetDisplayList: invalid list entry: " + str(Entry))
|
|
return ReturnValue
|
|
|
|
title, value = self.GetDisplayEntry(Entry, JSONNum = JSONNum, no_units=no_units)
|
|
|
|
if title == "default":
|
|
default = value
|
|
value = None
|
|
if title != None:
|
|
if value != None:
|
|
ReturnValue.append({title: value})
|
|
|
|
if not len(ReturnValue) and not default == None:
|
|
ReturnValue.append({"default": default})
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetDisplayList: (" + key_name + ") : " + str(e1))
|
|
return ReturnValue
|
|
return ReturnValue
|
|
|
|
# -------------CustomController:GetButtons--------------------------------
|
|
def GetButtons(self, singlebuttonname = None):
|
|
try:
|
|
button_list = self.controllerimport.get("buttons", None)
|
|
|
|
return self.GetButtonsCommon(button_list, singlebuttonname=singlebuttonname)
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetButtons: " + str(e1))
|
|
return []
|
|
|
|
# ---------- CustomController::SetGeneratorRemoteCommand--------------------
|
|
# CmdString will be in the format: "setremote=start"
|
|
# valid commands are defined in the JSON file
|
|
# return string "Remote command sent successfully" or some descriptive error
|
|
# string if failure
|
|
def SetGeneratorRemoteCommand(self, CmdString):
|
|
try:
|
|
|
|
try:
|
|
# Format we are looking for is "setremote=start"
|
|
CmdList = CmdString.split("=")
|
|
if len(CmdList) != 2:
|
|
self.LogError("Validation Error: Error parsing command string in SetGeneratorRemoteCommand (parse): "+ CmdString)
|
|
return "Error"
|
|
|
|
CmdList[0] = CmdList[0].strip()
|
|
|
|
if not CmdList[0].lower() == "setremote":
|
|
self.LogError("Validation Error: Error parsing command string in SetGeneratorRemoteCommand (parse2): "+ CmdString)
|
|
return "Error"
|
|
|
|
Command = CmdList[1].strip()
|
|
Command = Command.lower()
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Validation Error: Error parsing command string in SetGeneratorRemoteCommand: " + CmdString)
|
|
self.LogError(str(e1))
|
|
return "Error"
|
|
|
|
button_list = self.controllerimport.get("buttons", None)
|
|
|
|
if button_list == None:
|
|
return "No commands defined."
|
|
if not isinstance(button_list, list):
|
|
self.LogDebug("Error in SetGeneratorRemoteCommand: invalid input or data: "+ str(type(button_list)))
|
|
return "Malformed button in JSON file."
|
|
|
|
for button in button_list:
|
|
if button["onewordcommand"].lower() == Command.lower():
|
|
command_sequence = button["command_sequence"]
|
|
if not len(command_sequence):
|
|
self.LogDebug("Error in SetGeneratorRemoteCommand: invalid command sequence")
|
|
continue
|
|
|
|
with self.ModBus.CommAccessLock:
|
|
return self.ExecuteCommandSequence(command_sequence)
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in SetGeneratorRemoteCommand: " + str(e1))
|
|
return "Error"
|
|
return "Command not found."
|
|
|
|
# ------------ GeneratorController:GetDisplayEntry --------------------------
|
|
# return a title and value of an input dict describing the modbus register
|
|
# and type of value it is
|
|
def GetDisplayEntry(self, entry, JSONNum=False, no_units=False, inheritreg = None, inheritregtype = None):
|
|
|
|
ReturnTitle = ReturnValue = None
|
|
try:
|
|
Register = None
|
|
reg_type = None
|
|
if not isinstance(entry, dict):
|
|
self.LogError("Error: non dict passed to GetDisplayEntry: " + str(type(entry)))
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if "container" in entry.keys() and entry["container"] and "value" in entry.keys() and "title" in entry.keys():
|
|
ReturnValue = self.GetDisplayList(entry, "value", JSONNum=JSONNum)
|
|
return entry["title"], ReturnValue
|
|
if "inherit" in entry.keys() and inheritreg == None:
|
|
self.LogError("Error: inherit specified but no inherit value passed")
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if "iteration" in entry.keys():
|
|
ReturnValue = self.ProcessIterationObject(entry)
|
|
return entry["title"], ReturnValue
|
|
|
|
if "include" in entry.keys() and "include" in self.controllerimport.keys():
|
|
includeNames = entry["include"]
|
|
includeList = includeNames.split(",")
|
|
for includeName in includeList:
|
|
if includeName in self.controllerimport["include"].keys():
|
|
includeValue = self.controllerimport["include"][includeName]
|
|
if isinstance(includeValue, dict):
|
|
entry = self.MergeDicts(entry, includeValue)
|
|
else:
|
|
self.LogError("Error: include value is not an object: " + str(includeValue))
|
|
return ReturnTitle, ReturnValue
|
|
else:
|
|
self.LogError("Error: entry has include but object name is not in scope of base include object: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if "reg" not in entry.keys(): # required with exceptions
|
|
if "inherit" in entry.keys():
|
|
Register = inheritreg.lower() # add inherit register
|
|
reg_type = inheritregtype
|
|
elif entry["type"] != "list":
|
|
self.LogError("Error: reg not found in input to GetDisplayEntry: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
else:
|
|
if "reg_type" in entry.keys():
|
|
reg_type = entry["reg_type"]
|
|
if not reg_type in ["coil", "holding", "input", "file", "config"]:
|
|
self.LogError("Error : invalid reg_type in GetDisplayEntry:" + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
Register = entry["reg"].lower()
|
|
|
|
if reg_type != "config" and Register != None and not self.StringIsHex(Register):
|
|
self.LogError("Error: reg does not contain valid hex value in input to GetDisplayEntry: "+ str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if not "type" in entry.keys(): # required
|
|
self.LogError("Error: type not found in input to GetDisplayEntry: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if not "title" in entry.keys(): # required
|
|
self.LogError("Error: title not found in input to GetDisplayEntry: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if entry["type"] == "bits" and not "value" in entry.keys():
|
|
self.LogError("Error: value (requried for bits) not found in input to GetDisplayEntry: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
if entry["type"] == "bits" and not "text" in entry.keys():
|
|
self.LogError("Error: text not found in input to GetDisplayEntry: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
if entry["type"] == "float" and (not "multiplier" in entry.keys() and not "ieee754" in entry.keys()):
|
|
self.LogError("Error: multiplier or ieee754 (requried for float) not found in input to GetDisplayEntry: "+ str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
if entry["type"] == "regex" and not "regex" in entry.keys():
|
|
self.LogError("Error: regex not found in input to GetDisplayEntry: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
if "multiplier" in entry and entry["multiplier"] == 0:
|
|
self.LogError("Error: multiplier (requried for float) must not be zero in input to GetDisplayEntry: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
if (entry["type"] in ["int", "bits", "regex"] and not "mask" in entry): # required
|
|
self.LogError("Error: mask not found in input to GetDisplayEntry: " + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
elif "mask" in entry and not self.StringIsHex(entry["mask"]):
|
|
self.LogError("Error: mask does not contain valid hex value in input to GetDisplayEntry: "+ str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
if entry["type"] == "default" and not "text" in entry.keys():
|
|
self.LogError("Error: text (default) not found in input to GetDisplayEntry: "+ str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if entry["type"] != "list":
|
|
# check if we have read the register yet
|
|
if ((reg_type == None or reg_type == "holding") and
|
|
"holding_registers" in self.controllerimport.keys() and
|
|
Register not in self.Holding.keys()):
|
|
self.LogDebug("Holding Register not found: " + Register + " entry:" + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
if (reg_type == "input" and
|
|
"input_registers" in self.controllerimport.keys() and
|
|
Register not in self.Inputs.keys()):
|
|
self.LogDebug("Input Register not found: " + Register + " entry:" + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
if (reg_type == "coil" and
|
|
"coil_registers" in self.controllerimport.keys() and
|
|
Register not in self.Coils.keys()):
|
|
self.LogDebug("Coil Register not found: " + Register + " entry:" + str(entry))
|
|
return ReturnTitle, ReturnValue
|
|
ReturnTitle = entry["title"]
|
|
if "default" in entry.keys():
|
|
ReturnValue = entry["default"]
|
|
IsCoil = IsInput = False
|
|
if reg_type == "coil":
|
|
IsCoil = True
|
|
elif reg_type == "input":
|
|
IsInput = True
|
|
|
|
if "disable" in entry.keys():
|
|
# conditionally disable the entry
|
|
title, value = self.GetDisplayEntry(entry["disable"], JSONNum=JSONNum, no_units=no_units, inheritreg = Register, inheritregtype = reg_type)
|
|
if value:
|
|
#self.LogDebug("Disabling entry " + entry["title"])
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if "filter" in entry.keys():
|
|
# conditionally filter the entry
|
|
title, value = self.GetDisplayEntry(entry["filter"], JSONNum=JSONNum, no_units=no_units, inheritreg = Register, inheritregtype = reg_type)
|
|
if value != False:
|
|
#self.LogDebug("Filtering entry " + entry["title"] + ": " + str(value))
|
|
return ReturnTitle, value
|
|
|
|
# The entry is a config file read
|
|
if "reg_type" in entry and entry["reg_type"] == "config":
|
|
if entry["type"] == "bool":
|
|
ReturnValue = self.config.ReadValue(Register, return_type=bool, default=False)
|
|
elif entry["type"] == "int" or entry["type"] == "bits":
|
|
value = self.config.ReadValue(Register, return_type=int, default=ReturnValue)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
if entry["type"] == "bits":
|
|
if value == int(entry["value"], 16):
|
|
ReturnValue = entry["text"]
|
|
else:
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessSignedModifier(entry, value)
|
|
value = self.ProcessBitModifiers(entry, value)
|
|
if "bounds_regex" in entry.keys():
|
|
if re.match(entry["bounds_regex"], str(value)):
|
|
ReturnValue = self.ProcessExecModifier(entry, int(self.ProcessTemperatureModifier(entry, value)))
|
|
else:
|
|
ReturnValue = self.ProcessExecModifier(entry, int(self.ProcessTemperatureModifier(entry, value)))
|
|
elif entry["type"] == "float":
|
|
ReturnValue = self.config.ReadValue(Register, return_type=float, default=ReturnValue)
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
value = self.ProcessBitModifiers(entry, value, ReturnFloat=True)
|
|
value = self.ProcessTemperatureModifier( entry, value)
|
|
value = self.ProcessRoundModifiers(entry, value)
|
|
if "bounds_regex" in entry.keys():
|
|
if re.match(entry["bounds_regex"], str(float(value))):
|
|
ReturnValue = self.ProcessExecModifier(entry, float(value))
|
|
else:
|
|
ReturnValue = self.ProcessExecModifier(entry, float(value))
|
|
elif entry["type"] == "ascii":
|
|
ReturnValue = self.config.ReadValue(Register, default=ReturnValue)
|
|
return ReturnTitle, ReturnValue
|
|
|
|
if entry["type"] == "bits":
|
|
value = self.GetParameter(Register, ReturnInt=True, IsCoil=IsCoil, IsInput=IsInput)
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
if value == int(entry["value"], 16):
|
|
ReturnValue = entry["text"]
|
|
elif entry["type"] == "bool":
|
|
value = self.GetParameter(Register, ReturnInt=True, IsCoil=IsCoil, IsInput=IsInput)
|
|
if "value" in entry.keys():
|
|
if isinstance(entry["value"], str):
|
|
logical_value = (entry["value"].lower() == "true")
|
|
elif isinstance(entry["value"], bool):
|
|
logical_value = entry["value"]
|
|
else:
|
|
self.LogError("Error: for type bool 'value' must be a string or bool: " + str(entry))
|
|
logical_value = True
|
|
else:
|
|
logical_value = True
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
value = self.ProcessBitModifiers(entry, value)
|
|
if (not (value == 0)) == logical_value:
|
|
ReturnValue = entry["text"]
|
|
elif entry["type"] == "float":
|
|
value = self.GetParameter(Register, ReturnInt=True, IsCoil=IsCoil, IsInput=IsInput)
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
value = self.ProcessSignedModifier(entry, value)
|
|
value = self.ProcessBitModifiers(entry, value, ReturnFloat=True)
|
|
value = self.ProcessTemperatureModifier( entry, value)
|
|
value = self.ProcessRoundModifiers(entry, value)
|
|
if "bounds_regex" in entry.keys():
|
|
if re.match(entry["bounds_regex"], str(float(value))):
|
|
ReturnValue = self.ProcessExecModifier(entry, float(value))
|
|
else:
|
|
ReturnValue = self.ProcessExecModifier(entry, float(value))
|
|
elif entry["type"] == "int":
|
|
value = self.GetParameter(Register, ReturnInt=True, IsCoil=IsCoil, IsInput=IsInput)
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
value = self.ProcessSignedModifier(entry, value)
|
|
value = self.ProcessBitModifiers(entry, value)
|
|
if "bounds_regex" in entry.keys():
|
|
if re.match(entry["bounds_regex"], str(value)):
|
|
ReturnValue = self.ProcessExecModifier(entry, int(self.ProcessTemperatureModifier(entry, value)))
|
|
else:
|
|
ReturnValue = self.ProcessExecModifier(entry, int(self.ProcessTemperatureModifier(entry, value)))
|
|
elif entry["type"] == "regex":
|
|
regex_pattern = entry["regex"]
|
|
value = self.GetParameter(Register, ReturnInt=True, IsCoil=IsCoil, IsInput=IsInput)
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
value = self.ProcessBitModifiers(entry, value)
|
|
value = "%x" % value
|
|
result = re.match(regex_pattern, value)
|
|
if result:
|
|
ReturnValue = entry["text"]
|
|
|
|
elif entry["type"] == "list":
|
|
list_entry = entry["value"]
|
|
separator = ""
|
|
if "separator" in entry.keys():
|
|
separator = entry["separator"]
|
|
value_list = []
|
|
for item in list_entry:
|
|
title, value = self.GetDisplayEntry(item, JSONNum=False, inheritreg=inheritreg, inheritregtype=reg_type)
|
|
if value != None:
|
|
value_list.append(str(self.FormatEntry(item, value)))
|
|
# all list items must be present if format is used
|
|
if "format" in entry.keys():
|
|
if len(value_list) and len(value_list) == len(list_entry):
|
|
ReturnValue = entry["format"] % tuple(value_list)
|
|
else:
|
|
if separator == None or separator == "":
|
|
ReturnValue = self.ProcessExecModifier(entry, tuple(value_list ))
|
|
else:
|
|
ReturnValue = separator.join(value_list)
|
|
elif entry["type"] == "object_int_index":
|
|
value = self.GetParameter(Register, ReturnInt=True, IsCoil=IsCoil, IsInput=IsInput)
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
value = self.ProcessBitModifiers(entry, value)
|
|
if "default" in entry.keys():
|
|
obj_default = entry["default"]
|
|
else:
|
|
obj_default = None
|
|
ReturnValue = entry["object"].get(str(value), obj_default)
|
|
|
|
elif entry["type"] == "object_bit_index":
|
|
value = self.GetParameter(Register, ReturnInt=True, IsCoil=IsCoil, IsInput=IsInput)
|
|
value = self.ProcessEndianModifiers(entry, value)
|
|
value = self.ProcessMaskModifier(entry, value)
|
|
value = self.ProcessBitModifiers(entry, value)
|
|
if "default" in entry.keys():
|
|
obj_default = entry["default"]
|
|
else:
|
|
obj_default = None
|
|
index = 0
|
|
valueList = []
|
|
while(value):
|
|
if value & 0x01:
|
|
objval = entry["object"].get(str(index), None)
|
|
if objval != None:
|
|
valueList.append(objval)
|
|
value = value >> 1
|
|
index += 1
|
|
if len(valueList):
|
|
ReturnValue = ", ".join(valueList)
|
|
else:
|
|
ReturnValue = obj_default
|
|
|
|
elif entry["type"] == "ascii":
|
|
ReturnValue = self.GetParameter(Register, ReturnString = True, IsCoil=IsCoil, IsInput=IsInput)
|
|
elif entry["type"] == "default":
|
|
ReturnValue = entry["text"]
|
|
ReturnTitle = "default"
|
|
else:
|
|
self.LogError("Unknown type found in GetDisplayEntry: " + str(entry))
|
|
|
|
if not no_units and "units" in entry and ReturnValue != None:
|
|
units = entry["units"]
|
|
if units == None:
|
|
units = ""
|
|
else:
|
|
units = self.ProcessTemperatureModifier(entry, units, units = True)
|
|
ReturnValue = self.ValueOut(ReturnValue, str(units), JSONNum)
|
|
|
|
if "prepend_text" in entry.keys() and isinstance(ReturnValue, str):
|
|
ReturnValue = entry["prepend_text"] + ReturnValue
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetDisplayEntry : " + str(e1))
|
|
self.LogDebug(str(entry))
|
|
|
|
return ReturnTitle, ReturnValue
|
|
|
|
# ------------ GeneratorController:ProcessRoundModifiers ----------------------
|
|
def ProcessRoundModifiers(self, entry, value):
|
|
try:
|
|
if "round" in entry.keys():
|
|
return round(float(value), int(entry["round"]))
|
|
else:
|
|
return value
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ProcessRoundModifiers: " + str(e1) + ": " + str(entry["title"]))
|
|
return value
|
|
# ------------ GeneratorController:ProcessEndianModifiers -------------------
|
|
def ProcessEndianModifiers(self, entry, value, ReturnFloat = False):
|
|
try:
|
|
if "swapwords32" in entry.keys():
|
|
# change from 01234567 to 456701234
|
|
if entry["swapwords32"] == True:
|
|
value = self.SwapWords32(value)
|
|
return value
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ProcessEndianModifiers: " + str(e1) + ": " + str(entry["title"]))
|
|
return value
|
|
|
|
# ------------ GeneratorController:ProcessBitModifiers ----------------------
|
|
def ProcessBitModifiers(self, entry, value, ReturnFloat = False):
|
|
try:
|
|
orignialvalue = 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:ProcessSignedModifier---------------------
|
|
def ProcessSignedModifier(self, entry, value):
|
|
|
|
try:
|
|
bitdepth = None
|
|
if "signed16" in entry.keys() and entry["signed16"] == True:
|
|
bitdepth = 16
|
|
elif "signed32" in entry.keys() and entry["signed32"] == True:
|
|
bitdepth = 32
|
|
value = self.getSignedNumber( value, bitdepth)
|
|
return value
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ProcessSignedModifier: " + str(e1) + ": " + str(entry["title"]))
|
|
return value
|
|
|
|
# ------------ GeneratorController:ProcessMaskModifier ----------------------
|
|
def ProcessMaskModifier(self, entry, value):
|
|
try:
|
|
if "default" in entry.keys():
|
|
ReturnValue = entry["default"]
|
|
else:
|
|
ReturnValue = value
|
|
if not "mask" in entry.keys():
|
|
return value
|
|
value = value & int(entry["mask"], 16)
|
|
return value
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ProcessMaskModifier: " + str(e1) + ": " + str(entry["title"]))
|
|
return ReturnValue
|
|
|
|
# ------------ GeneratorController:ProcessIterationObject ------------------
|
|
def ProcessIterationObject(self, entry):
|
|
try:
|
|
if "reg" in entry.keys():
|
|
Register = entry["reg"]
|
|
if "reg_type" in entry.keys():
|
|
reg_type = entry["reg_type"]
|
|
else:
|
|
reg_type = None # holding register
|
|
RegisterInt = int(Register,16)
|
|
if not "iteration" in entry.keys():
|
|
self.LogError("Error in ProcessIterationObject: reg present but not iteration: " + str(entry))
|
|
return None
|
|
if not "step" in entry.keys():
|
|
self.LogError("Error in ProcessIterationObject: reg present but not step: " + str(entry))
|
|
return None
|
|
iteration = entry["iteration"]
|
|
LogList = []
|
|
while iteration > 0:
|
|
Register = "%04x" % RegisterInt
|
|
title, LogResults = self.GetDisplayEntry(entry["object"], inheritreg=Register, inheritregtype = reg_type)
|
|
if LogResults != None and len(LogResults):
|
|
LogList.append(LogResults)
|
|
RegisterInt += entry["step"]
|
|
iteration -= 1
|
|
return LogList
|
|
else:
|
|
# Non inherit
|
|
return None
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ProcessIterationObject: " + str(e1) + ": " + str(entry["title"]))
|
|
return None
|
|
|
|
# ------------ GeneratorController:ProcessExecModifier ----------------------
|
|
def ProcessExecModifier(self, entry, value, altname = None):
|
|
try:
|
|
exec_string = ""
|
|
if "default" in entry.keys():
|
|
ReturnValue = entry["default"]
|
|
else:
|
|
ReturnValue = value
|
|
|
|
execname = "exec"
|
|
if not altname == None:
|
|
execname = altname
|
|
if not execname in entry.keys():
|
|
if altname != None:
|
|
self.LogDebug("Error in ProcessExecModifier: execname not found in object:" + str(execname) + ": " + str(entry))
|
|
return value
|
|
|
|
if isinstance(value, tuple):
|
|
exec_string = entry[execname].format(*value)
|
|
else:
|
|
exec_string = entry[execname].format(value)
|
|
exec_out = value
|
|
localsparam = {'exec_out': exec_out}
|
|
exec(exec_string, globals(), localsparam)
|
|
return localsparam["exec_out"]
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ProcessExecModifier: " + str(e1) + ": " + str(entry["title"]))
|
|
self.LogDebug(f"ProcessExecModifier exec_string: {exec_string}")
|
|
return ReturnValue
|
|
|
|
# ------------ GeneratorController:ProcessTemperatureModifier --------------
|
|
def ProcessTemperatureModifier(self, entry, value, units = False):
|
|
try:
|
|
if "temperature" in entry.keys():
|
|
if not units:
|
|
if not self.UseMetric and entry["temperature"].lower() == "celsius":
|
|
return self.ConvertCelsiusToFahrenheit(value)
|
|
elif self.UseMetric and entry["temperature"].lower() == "fahrenheit":
|
|
return self.ConvertFahrenheitToCelsius(value)
|
|
else:
|
|
return value
|
|
else:
|
|
if not self.UseMetric and entry["temperature"].lower() == "celsius":
|
|
return "F"
|
|
elif self.UseMetric and entry["temperature"].lower() == "fahrenheit":
|
|
return "C"
|
|
else:
|
|
return value
|
|
else:
|
|
return value
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in ProcessTemperatureModifier : " + str(e1))
|
|
return value
|
|
|
|
# ------------ GeneratorController:FormatEntry ------------------------------
|
|
def FormatEntry(self, entry, value):
|
|
try:
|
|
if "format" in entry.keys():
|
|
if entry["type"] == "float":
|
|
return str(entry["format"] % float(value))
|
|
elif entry["type"] == "int":
|
|
return str(entry["format"] % int(value))
|
|
else:
|
|
return value
|
|
else:
|
|
return value
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in FormatEntry : " + str(e1))
|
|
return value
|
|
|
|
# ------------ GeneratorController:GetRunHours ------------------------------
|
|
# return a string with no units of run hours
|
|
def GetRunHours(self):
|
|
try:
|
|
ReturnValue, run_hours = self.GetSingleEntry("run_hours")
|
|
if not ReturnValue:
|
|
return "0"
|
|
run_hours = self.removeAlpha(str(run_hours))
|
|
return run_hours
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetRunHours : " + str(e1))
|
|
return "0"
|
|
|
|
# ------------------- CustomController::DisplayOutage -----------------------
|
|
def DisplayOutage(self, DictOut=False, JSONNum=False):
|
|
|
|
try:
|
|
|
|
Outage = collections.OrderedDict()
|
|
Outage["Outage"] = []
|
|
|
|
if not self.OutageSupported:
|
|
Outage["Outage"].append({"Status": "No Supported"})
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Outage, ""))
|
|
return Outage
|
|
|
|
if self.SystemInOutage:
|
|
outstr = "System in outage since %s" % self.OutageStartTime.strftime(
|
|
"%Y-%m-%d %H:%M:%S"
|
|
)
|
|
else:
|
|
if self.ProgramStartTime != self.OutageStartTime:
|
|
OutageStr = str(self.LastOutageDuration).split(".")[
|
|
0
|
|
] # remove microseconds from string
|
|
outstr = "Last outage occurred at %s and lasted %s." % (
|
|
self.OutageStartTime.strftime("%Y-%m-%d %H:%M:%S"),
|
|
OutageStr,
|
|
)
|
|
else:
|
|
outstr = "No outage has occurred since program launched."
|
|
|
|
Outage["Outage"].append({"Status": outstr})
|
|
Outage["Outage"].append(
|
|
{"System In Outage": "Yes" if self.SystemInOutage else "No"}
|
|
)
|
|
|
|
# get utility voltage
|
|
|
|
bSuccess, UtilityVoltage = self.GetSingleEntry("linevoltage")
|
|
if bSuccess:
|
|
Outage["Outage"].append(
|
|
{"Utility Voltage": self.ValueOut(UtilityVoltage, "V", JSONNum)}
|
|
)
|
|
Outage["Outage"].append(
|
|
{
|
|
"Utility Voltage Minimum": self.ValueOut(
|
|
self.UtilityVoltsMin, "V", JSONNum
|
|
)
|
|
}
|
|
)
|
|
Outage["Outage"].append(
|
|
{
|
|
"Utility Voltage Maximum": self.ValueOut(
|
|
self.UtilityVoltsMax, "V", JSONNum
|
|
)
|
|
}
|
|
)
|
|
|
|
bSuccess, ThresholdVoltage = self.GetSingleEntry("thresholdvoltage")
|
|
if bSuccess:
|
|
Outage["Outage"].append(
|
|
{
|
|
"Utility Threshold Voltage": self.ValueOut(
|
|
int(ThresholdVoltage), "V", JSONNum
|
|
)
|
|
}
|
|
)
|
|
|
|
bSuccess, PickupVoltage = self.GetSingleEntry("pickupvoltage")
|
|
if bSuccess:
|
|
Outage["Outage"].append(
|
|
{
|
|
"Utility Pickup Voltage": self.ValueOut(
|
|
int(PickupVoltage), "V", JSONNum
|
|
)
|
|
}
|
|
)
|
|
|
|
Outage["Outage"].append({"Outage Log": self.DisplayOutageHistory(JSONNum=JSONNum)})
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayOutage: " + str(e1))
|
|
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Outage, ""))
|
|
|
|
return Outage
|
|
|
|
# ------------ CustomController::DisplayRegisters ---------------------------
|
|
def DisplayRegisters(self, AllRegs=False, DictOut=False):
|
|
|
|
try:
|
|
Registers = collections.OrderedDict()
|
|
Regs = collections.OrderedDict()
|
|
Registers["Registers"] = Regs
|
|
|
|
RegList = []
|
|
InputList = []
|
|
CoilList = []
|
|
|
|
Regs["Num Regs"] = "%d" % (len(self.Holding) + len(self.Inputs) + len(self.Coils))
|
|
|
|
Regs["Holding"] = RegList
|
|
# display all the registers
|
|
temp_regsiters = self.Holding
|
|
for Register, Value in temp_regsiters.items():
|
|
isLog, LogRegLength, Name = self.RegisterIsLog(Register)
|
|
if AllRegs or not isLog:
|
|
RegList.append({Register: Value})
|
|
|
|
Regs["Inputs"] = InputList
|
|
# display all the registers
|
|
temp_regsiters = self.Inputs
|
|
for Register, Value in temp_regsiters.items():
|
|
InputList.append({Register: Value})
|
|
|
|
Regs["Coils"] = CoilList
|
|
# display all the registers
|
|
temp_regsiters = self.Coils
|
|
for Register, Value in temp_regsiters.items():
|
|
CoilList.append({Register: Value})
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in DisplayRegisters: " + str(e1))
|
|
|
|
if not DictOut:
|
|
return self.printToString(self.ProcessDispatch(Registers, ""))
|
|
|
|
return Registers
|
|
|
|
# ---------- CustomController: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):
|
|
|
|
if Actual:
|
|
if "model" in self.controllerimport.keys():
|
|
ReturnValue, DetectedModel = self.GetSingleEntry("model")
|
|
if ReturnValue and isinstance(DetectedModel,str):
|
|
return DetectedModel
|
|
|
|
return self.Model # this should be the "controller_name" object
|
|
|
|
# ---------- CustomController:GetVoltageOutput ---------------------------
|
|
def GetVoltageOutput(self, ReturnInt=False):
|
|
|
|
try:
|
|
# get Output Voltage
|
|
bSuccess, Voltage = self.GetSingleEntry("outputvoltage")
|
|
if not bSuccess:
|
|
return None
|
|
|
|
if ReturnInt:
|
|
return int(self.removeAlpha(Voltage))
|
|
return Voltage
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetVoltageOutput: " + str(e1))
|
|
return None
|
|
|
|
# ---------- CustomController:GetBatteryVoltage --------------------------
|
|
def GetBatteryVoltage(self, ReturnFloat=False):
|
|
try:
|
|
# get Battery Voltage
|
|
bSuccess, Voltage = self.GetSingleEntry("batteryvoltage")
|
|
if not bSuccess:
|
|
return None
|
|
|
|
if ReturnFloat:
|
|
return float(self.removeAlpha(Voltage))
|
|
return Voltage
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetBatteryVoltage: " + str(e1))
|
|
return None
|
|
|
|
# ---------- CustomController:GetUtilityVoltage --------------------------
|
|
def GetUtilityVoltage(self, ReturnInt=False):
|
|
try:
|
|
# get Utility Voltage
|
|
bSuccess, Voltage = self.GetSingleEntry("linevoltage")
|
|
if not bSuccess:
|
|
return None
|
|
|
|
if ReturnInt:
|
|
return int(self.removeAlpha(Voltage))
|
|
return Voltage
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetUtilityVoltage: " + str(e1))
|
|
return None
|
|
|
|
# ---------- CustomController:GetFrequency -------------------------------
|
|
def GetFrequency(self, Calculate=False, ReturnFloat=False):
|
|
try:
|
|
# get Frequency
|
|
bSuccess, Frequency = self.GetSingleEntry("frequency")
|
|
if not bSuccess:
|
|
return None
|
|
|
|
if ReturnFloat:
|
|
return float(self.removeAlpha(Frequency))
|
|
return Frequency
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetFrequency: " + str(e1))
|
|
return None
|
|
|
|
# ---------- CustomController:GetRPM -------------------------------------
|
|
def GetRPM(self, ReturnInt=True):
|
|
try:
|
|
# get Utility Voltage
|
|
bSuccess, RPM = self.GetSingleEntry("rpm")
|
|
if not bSuccess:
|
|
return None
|
|
|
|
if ReturnInt:
|
|
return int(self.removeAlpha(RPM))
|
|
return RPM
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetRPM: " + str(e1))
|
|
return None
|
|
|
|
# ---------- CustomController:ComminicationsIsActive ---------------------
|
|
# Called every few seconds, if communictions are failing, return False, otherwise
|
|
# True
|
|
def ComminicationsIsActive(self):
|
|
if self.LastRxPacketCount == self.ModBus.RxPacketCount:
|
|
return False
|
|
else:
|
|
self.LastRxPacketCount = self.ModBus.RxPacketCount
|
|
return True
|
|
|
|
# ---------- CustomController:RemoteButtonsSupported ----------------------
|
|
# return true if Panel buttons are settable via the software
|
|
def RemoteButtonsSupported(self):
|
|
return False
|
|
|
|
# ---------- CustomController:PowerMeterIsSupported -----------------------
|
|
# return true if GetPowerOutput is supported
|
|
def PowerMeterIsSupported(self):
|
|
|
|
if self.bDisablePowerLog:
|
|
return False
|
|
if self.UseExternalCTData:
|
|
return True
|
|
|
|
if "power" in self.controllerimport.keys():
|
|
return True
|
|
return False
|
|
|
|
# ---------------------CustomController::GetPowerOutput----------------------
|
|
# returns current kW
|
|
# rerturn empty string ("") if not supported,
|
|
# return kW with units i.e. "2.45kW"
|
|
def GetPowerOutput(self, ReturnFloat=False):
|
|
|
|
try:
|
|
if ReturnFloat:
|
|
DefaultReturn = 0.0
|
|
else:
|
|
DefaultReturn = "0 kW"
|
|
|
|
if not self.PowerMeterIsSupported():
|
|
return DefaultReturn
|
|
|
|
ReturnValue = self.CheckExternalCTData(
|
|
request="power", ReturnFloat=ReturnFloat
|
|
)
|
|
if ReturnValue != None:
|
|
return ReturnValue
|
|
|
|
bSuccess, PowerValue = self.GetSingleEntry("power")
|
|
if not bSuccess or PowerValue == None:
|
|
return DefaultReturn
|
|
|
|
if isinstance(PowerValue, float):
|
|
PowerValue = round(PowerValue, 2)
|
|
|
|
if ReturnFloat:
|
|
return float(PowerValue)
|
|
return PowerValue
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetPowerOutput: " + str(e1))
|
|
return "Unknown"
|
|
|
|
# ------------ CustomController:CheckExternalCTData -------------------------
|
|
def CheckExternalCTData(self, request="current", ReturnFloat=False, gauge=False):
|
|
try:
|
|
|
|
if ReturnFloat:
|
|
DefaultReturn = 0.0
|
|
else:
|
|
DefaultReturn = 0
|
|
|
|
if not self.UseExternalCTData:
|
|
return None
|
|
ExternalData = self.GetExternalCTData()
|
|
|
|
if ExternalData == None:
|
|
return None
|
|
|
|
# This assumes the following format:
|
|
# NOTE: all fields are *optional*
|
|
# { "strict" : True or False (true requires an outage to use the data)
|
|
# "current" : optional, float value in amps
|
|
# "power" : optional, float value in kW
|
|
# "powerfactor" : float value (default is 1.0) used if converting from current to power or power to current
|
|
# ctdata[] : list of amps for each leg
|
|
# ctpower[] : list of power in kW for each leg
|
|
# voltagelegs[] : list of voltage legs
|
|
# voltage : optional, float value of total RMS voltage (all legs combined)
|
|
# phase : optional, int (1 or 3)
|
|
# }
|
|
strict = False
|
|
if "strict" in ExternalData:
|
|
strict = ExternalData["strict"]
|
|
|
|
if strict:
|
|
if not self.SystemInOutage:
|
|
if gauge:
|
|
return DefaultReturn
|
|
else:
|
|
return None
|
|
|
|
# if we get here we must convert the data.
|
|
if not "outputvoltage" in self.controllerimport.keys():
|
|
self.LogDebug(
|
|
"WARNING: no outputvoltage in custom controller defintion"
|
|
)
|
|
Voltage = None
|
|
else:
|
|
bSuccess, Voltage = self.GetSingleEntry("outputvoltage")
|
|
if not bSuccess:
|
|
return DefaultReturn
|
|
|
|
if isinstance(Voltage, str):
|
|
# TODO why is this needed?
|
|
Voltage = int(self.removeAlpha(Voltage))
|
|
|
|
return self.ConvertExternalData(
|
|
request=request, voltage=Voltage, ReturnFloat=ReturnFloat
|
|
)
|
|
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in CheckExternalCTData: " + str(e1))
|
|
return DefaultReturn
|
|
|
|
# ------------ CustomController:GetBaseStatus -------------------------------
|
|
# return one of the following: "ALARM", "SERVICEDUE", "EXERCISING", "RUNNING",
|
|
# "RUNNING-MANUAL", "OFF", "MANUAL", "READY"
|
|
def GetBaseStatus(self):
|
|
try:
|
|
EngineStatus = self.GetEngineState().lower()
|
|
GeneratorStatus = self.GetGeneratorStatus().lower()
|
|
SwitchState = self.GetSwitchState().lower()
|
|
|
|
validStates = ["ALARM", "SERVICEDUE", "EXERCISING", "RUNNING","RUNNING-MANUAL", "OFF", "MANUAL", "READY"]
|
|
if "basestatus" in self.controllerimport.keys():
|
|
ReturnStatus = self.GetExtendedDisplayString(self.controllerimport,"basestatus", FirstItemOnly = True)
|
|
if ReturnStatus != "Unknown":
|
|
ReturnStatus = ReturnStatus.upper()
|
|
if ReturnStatus in validStates:
|
|
return ReturnStatus
|
|
|
|
if "running" in EngineStatus:
|
|
IsRunning = True
|
|
else:
|
|
IsRunning = False
|
|
if "stopped" in GeneratorStatus:
|
|
IsStopped = True
|
|
else:
|
|
IsStopped = False
|
|
|
|
ExerciseList = ["exercising", "exercise", "quiettest", "test"]
|
|
if (
|
|
any(x in EngineStatus for x in ExerciseList)
|
|
or any(x in GeneratorStatus for x in ExerciseList)
|
|
or any(x in SwitchState for x in ExerciseList)
|
|
):
|
|
IsExercising = True
|
|
else:
|
|
IsExercising = False
|
|
if "service" in EngineStatus or "service" in GeneratorStatus:
|
|
ServiceDue = True
|
|
elif "maintenance_due" in self.controllerimport:
|
|
ServiceStr = self.GetExtendedDisplayString(self.controllerimport, "maintenance_due")
|
|
if ServiceStr == "Unknown" or ServiceStr == "" or ServiceStr == None:
|
|
ServiceDue = False
|
|
else:
|
|
ServiceDue = True
|
|
else:
|
|
ServiceDue = False
|
|
|
|
if self.SystemInAlarm():
|
|
return "ALARM"
|
|
elif ServiceDue:
|
|
return "SERVICEDUE"
|
|
elif IsExercising:
|
|
return "EXERCISING"
|
|
elif IsRunning and SwitchState.startswith("auto") or "run" in GeneratorStatus.lower() or "run" in EngineStatus.lower():
|
|
return "RUNNING"
|
|
elif IsRunning and SwitchState.startswith("manual"):
|
|
return "RUNNING-MANUAL"
|
|
elif SwitchState.startswith("manual"):
|
|
return "MANUAL"
|
|
elif SwitchState.startswith("auto"):
|
|
return "READY"
|
|
elif SwitchState == "off" or SwitchState == "stop":
|
|
return "OFF"
|
|
elif SwitchState.lower() == "inactive" or EngineStatus.lower() == "inactive" or GeneratorStatus.lower() == "inactive":
|
|
return "READY"
|
|
else:
|
|
if self.InitComplete:
|
|
message = (
|
|
"Unknown Base State: "
|
|
+ str(EngineStatus)
|
|
+ ": "
|
|
+ str(GeneratorStatus)
|
|
+ ": "
|
|
+ str(SwitchState)
|
|
)
|
|
self.FeedbackPipe.SendFeedback(
|
|
"Base State", FullLogs=True, Always=True, Message=message
|
|
)
|
|
return "UNKNOWN"
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetBaseStatus: " + str(e1))
|
|
return "UNKNOWN"
|
|
|
|
# ---------- CustomController::FuelSensorSupported--------------------------
|
|
def FuelSensorSupported(self):
|
|
|
|
if "fuel" in self.controllerimport.keys():
|
|
ReturnVal, FuelValue = self.GetSingleEntry("fuel")
|
|
if FuelValue != None:
|
|
return True
|
|
|
|
return False
|
|
|
|
# ------------ CustomController:GetFuelSensor -------------------------------
|
|
def GetFuelSensor(self, ReturnInt=False):
|
|
|
|
if not self.FuelSensorSupported():
|
|
return None
|
|
|
|
try:
|
|
ReturnVal, FuelValue = self.GetSingleEntry("fuel")
|
|
if not ReturnVal or FuelValue == None:
|
|
return None
|
|
# This should never return greater than 100%
|
|
if int(FuelValue) > 100:
|
|
FuelValue = "100"
|
|
elif int(FuelValue) < 0:
|
|
FuelValue = "0"
|
|
if ReturnInt:
|
|
return int(FuelValue)
|
|
return FuelValue
|
|
except Exception as e1:
|
|
self.LogErrorLine("Error in GetFuelSensor: " + str(e1))
|
|
return None
|
|
|
|
# ---------- CustomController::GetFuelConsumptionDataPoints-----------------
|
|
def GetFuelConsumptionDataPoints(self):
|
|
|
|
try:
|
|
if self.FuelHalfRate == 0 or self.FuelFullRate == 0:
|
|
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
|