Files
HenryPump-Drivers/plcpond/plcpond.py
2022-03-09 15:34:08 -06:00

356 lines
17 KiB
Python

"""Driver for plcpond"""
import threading
import sys
import json
import time
import logging
from random import randint
from device_base import deviceBase
from Channel import PLCChannel, read_tag, write_tag, TAG_DATAERROR_SLEEPTIME
from utilities import get_public_ip_address, get_private_ip_address
from datetime import datetime as dt
import persistence
_ = None
# LOGGING SETUP
from logging.handlers import RotatingFileHandler
log_formatter = logging.Formatter('%(asctime)s %(levelname)s %(funcName)s(%(lineno)d) %(message)s')
logFile = './plcpond.log'
my_handler = RotatingFileHandler(logFile, mode='a', maxBytes=500*1024, backupCount=2, encoding=None, delay=0)
my_handler.setFormatter(log_formatter)
my_handler.setLevel(logging.INFO)
logger = logging.getLogger('plcpond')
logger.setLevel(logging.INFO)
logger.addHandler(my_handler)
console_out = logging.StreamHandler(sys.stdout)
console_out.setFormatter(log_formatter)
logger.addHandler(console_out)
logger.info("plcpond startup")
# GLOBAL VARIABLES
WATCHDOG_SEND_PERIOD = 3600 # Seconds, the longest amount of time before sending the watchdog status
PLC_IP_ADDRESS = "192.168.1.12"
PERSIST = persistence.load('persist.json')
if not PERSIST:
PERSIST = {
'flowmeter_enable': False
}
persistence.store(PERSIST, 'persist.json')
TOTALIZER = persistence.load('totalizers.json')
if not TOTALIZER:
TOTALIZER = {
'Todays': 0,
'Yesterdays': 0,
'Current Months': 0,
'Previous Months': 0,
'Monthly Holding': 0,
'Daily Holding': 0,
'Lifetime': 0,
'Day': 0,
'Month': 0,
'Last Report': 0
}
persistence.store(TOTALIZER, 'totalizers.json')
CHANNELS = [
PLCChannel(PLC_IP_ADDRESS, "cfgnumberofponds", "cfgNumberOfPonds", "REAL", 0.5, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pond1height", "pond1Height", "REAL", 5.0, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pond2height", "pond2Height", "REAL", 5.0, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pond3height", "pond3Height", "REAL", 5.0, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pond4height", "pond4Height", "REAL", 5.0, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pond1volume", "pond1Volume", "REAL", 10000.0, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pond2volume", "pond2Volume", "REAL", 10000.0, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pond3volume", "pond3Volume", "REAL", 10000.0, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pond4volume", "pond4Volume", "REAL", 10000.0, 3600, map_=False, write_enabled=False, plc_type='Micro800'),
PLCChannel(PLC_IP_ADDRESS, "pondvolumetotal", "pondVolumeTotal", "REAL", 200000.0, 3600, map_=False, write_enabled=False, plc_type='Micro800')
]
if PERSIST['flowmeter_enable']:
CHANNELS.append(PLCChannel(PLC_IP_ADDRESS, 'volume_flow', 'Val_FlowMeterFR', 'REAL', 500, 3600, plc_type='Micro800'))
CHANNELS.append(PLCChannel(PLC_IP_ADDRESS, 'totalizer_1', 'Val_FlowMeterT1', 'REAL', 1000, 3600, plc_type='Micro800'))
CHANNELS.append(PLCChannel(PLC_IP_ADDRESS, 'totalizer_2', 'Val_FlowMeterT2', 'REAL', 1000, 3600, plc_type='Micro800'))
CHANNELS.append(PLCChannel(PLC_IP_ADDRESS, 'totalizer_3', 'Val_FlowMeterT3', 'REAL', 1000, 3600, plc_type='Micro800'))
#CHANNELS.append(PLCChannel(PLC_IP_ADDRESS, 'today_flow', 'Val_FlowMeterToday', 'REAL', 1000, 3600, plc_type='Micro800'))
#CHANNELS.append(PLCChannel(PLC_IP_ADDRESS, 'yesterday_flow', 'Val_FlowMeterYesterday', 'REAL', 1000, 3600, plc_type='Micro800'))
#CHANNELS.append(PLCChannel(PLC_IP_ADDRESS, 'current_month_flow', 'Val_FlowMeterMonth', 'REAL', 1000, 3600, plc_type='Micro800'))
#CHANNELS.append(PLCChannel(PLC_IP_ADDRESS, 'prev_month_flow', 'Val_FlowMeterLastMonth ', 'REAL', 1000, 3600, plc_type='Micro800'))
CALIBRATION_TABLES = [[],[], [], [], []] # position 0 is a dummy table
class start(threading.Thread, deviceBase):
"""Start class required by Meshify."""
def __init__(self, name=None, number=None, mac=None, Q=None, mcu=None, companyId=None, offset=None, mqtt=None, Nodes=None):
"""Initialize the driver."""
threading.Thread.__init__(self)
deviceBase.__init__(self, name=name, number=number, mac=mac, Q=Q, mcu=mcu, companyId=companyId, offset=offset, mqtt=mqtt, Nodes=Nodes)
self.daemon = True
self.version = "7"
self.finished = threading.Event()
self.force_send = False
threading.Thread.start(self)
# this is a required function for all drivers, its goal is to upload some piece of data
# about your device so it can be seen on the web
def register(self):
"""Register the driver."""
# self.sendtodb("log", "BOOM! Booted.", 0)
pass
def run(self):
"""Actually run the driver."""
wait_sec = 30
for i in range(0, wait_sec):
print("plcpond driver will start in {} seconds".format(wait_sec - i))
time.sleep(1)
logger.info("BOOM! Starting plcpond driver...")
public_ip_address = get_public_ip_address()
self.sendtodbDev(1, 'public_ip_address', public_ip_address, 0, 'plcpond')
private_ip_address = get_private_ip_address()
self.sendtodbDev(1, 'private_ip_address', private_ip_address, 0, 'plcpond')
watchdog = self.plcpond_watchdog()
self.sendtodbDev(1, 'watchdog', watchdog, 0, 'plcpond')
watchdog_send_timestamp = time.time()
self.nodes["plcpond_0199"] = self
send_loops = 0
watchdog_loops = 0
watchdog_check_after = 5000
while True:
try:
if self.force_send:
logger.warning("FORCE SEND: TRUE")
for c in CHANNELS:
v = c.read()
if v is not None: # read returns None if it fails
if c.mesh_name in ["totalizer_1"]:
self.totalize(v)
if c.check(v, self.force_send):
self.sendtodbDev(1, c.mesh_name, c.value, 0, 'plcpond')
#time.sleep(TAG_DATAERROR_SLEEPTIME) # sleep to allow Micro800 to handle ENET requests
for pond_index in range(1, 5):
self.read_pond_calibration(pond_index)
# print("plcpond driver still alive...")
if self.force_send:
if send_loops > 2:
logger.warning("Turning off force_send")
self.force_send = False
send_loops = 0
else:
send_loops += 1
watchdog_loops += 1
if watchdog_loops >= watchdog_check_after:
test_watchdog = self.plcpond_watchdog()
if not test_watchdog == watchdog or (time.time() - watchdog_send_timestamp) > WATCHDOG_SEND_PERIOD:
self.sendtodbDev(1, 'watchdog', test_watchdog, 0, 'plcpond')
watchdog = test_watchdog
test_public_ip = get_public_ip_address()
if not test_public_ip == public_ip_address:
self.sendtodbDev(1, 'public_ip_address', test_public_ip, 0, 'plcpond')
public_ip_address = test_public_ip
watchdog_loops = 0
except Exception as e:
logger.error("Something went wrong in read: {}".format(e))
def read_pond_calibration(self, pond_number):
"""Read all calibration values for a specific pond."""
last_read_height = -1.0
cal_values = []
cal_index = 1
while last_read_height != 0 and cal_index <=10:
cal_tag_height = "pond{}CalibrationHeight[{}]".format(pond_number, cal_index)
cal_tag_volume = "pond{}CalibrationVolume[{}]".format(pond_number, cal_index)
read_height = read_tag(PLC_IP_ADDRESS, cal_tag_height, plc_type="Micro800")
#time.sleep(2)
read_volume = read_tag(PLC_IP_ADDRESS, cal_tag_volume, plc_type="Micro800")
#time.sleep(2)
if read_height and read_volume:
if read_height[0] > 0.0:
cal_values.append({"height": read_height[0], "volume": read_volume[0]})
last_read_height = read_height[0]
cal_index += 1
if cal_values != CALIBRATION_TABLES[pond_number] or self.force_send:
calibration_channel = "pond{}calibration".format(pond_number)
calibration_string = json.dumps(cal_values).replace('"', "'")
self.sendtodbDev(1, calibration_channel, calibration_string, 0, 'plcpond')
CALIBRATION_TABLES[pond_number] = cal_values
def plcpond_watchdog(self):
"""Write a random integer to the PLC and then 1 seconds later check that it has been decremented by 1."""
randval = randint(0, 32767)
write_tag(str(PLC_IP_ADDRESS), 'watchdog_INT', randval, plc_type="Micro800")
time.sleep(1)
watchdog_val = read_tag(str(PLC_IP_ADDRESS), 'watchdog_INT', plc_type="Micro800")
try:
return (randval - 1) == watchdog_val[0]
except (KeyError, TypeError):
return False
def plcpond_sync(self, name, value):
"""Sync all data from the driver."""
self.force_send = True
# self.sendtodb("log", "synced", 0)
return True
def plcpond_deletecalibrationpoint(self, name, value):
"""Delete a calibration point from a calibration table"""
# {"pond": int, "point": int}
value = value.replace("'", '"')
parsed = json.loads(value)
try:
pond_number = int(parsed['pond'])
point_number = int(parsed['point'])
write_pond = write_tag(PLC_IP_ADDRESS, "inpPondNumber", pond_number, plc_type="Micro800")
#time.sleep(2)
write_point = write_tag(PLC_IP_ADDRESS, "inpDeletePointIndex", point_number, plc_type="Micro800")
#time.sleep(2)
if write_pond and write_point:
write_cmd = write_tag(PLC_IP_ADDRESS, "cmdDeleteCalibrationPoint", 1, plc_type="Micro800")
#time.sleep(2)
if write_cmd:
read_val = read_tag(PLC_IP_ADDRESS, "deleteSuccess", plc_type="Micro800")
if read_val[0] == 1:
self.read_pond_calibration(pond_number)
return True
return "Wrote everything successfully, but delete didn't succeed (Check pond and point values)."
return "Didn't write delete command correctly."
return "Didn't write pond or point correctly."
except KeyError as e:
return "Couldn't parse input value: {} -- {}".format(value, e)
def plcpond_cfgnumberofponds(self, name, value):
"""Write the number of ponds to the plc."""
value = int(value)
return write_tag(PLC_IP_ADDRESS, "cfgNumberOfPonds", value, plc_type="Micro800")
def plcpond_addcalibrationpoint(self, name, value):
"""Add a calibration point to the calibration table"""
# value = {"pond": int, "height": float, "volume": float}
value = value.replace("'", '"')
parsed = json.loads(value)
try:
# parse json values, throw an error if one is missing
pond_number = int(parsed['pond'])
height = float(parsed['height'])
volume = float(parsed['volume'])
# write values to the tags
write_pond = write_tag(PLC_IP_ADDRESS, "inpPondNumber", pond_number, plc_type="Micro800")
#time.sleep(2)
write_height = write_tag(PLC_IP_ADDRESS, "inpPondHeight", height, plc_type="Micro800")
#time.sleep(2)
write_volume= write_tag(PLC_IP_ADDRESS, "inpPondVolume", volume, plc_type="Micro800")
#time.sleep(2)
if write_pond and write_height and write_volume:
write_cmd = write_tag(PLC_IP_ADDRESS, "cmdAddCalibrationPoint", 1, plc_type="Micro800")
#time.sleep(2)
if write_cmd:
read_val = read_tag(PLC_IP_ADDRESS, "addSuccess", plc_type="Micro800")
if read_val[0] == 1:
self.read_pond_calibration(pond_number)
return True
return "Wrote everything successfully, but delete didn't succeed (Check pond and point values)."
return "Didn't write delete command correctly."
return "Didn't write pond or point correctly."
except KeyError as e:
return "Couldn't parse input value: {} -- {}".format(value, e)
def plcpond_writeplctag(self, name, value):
"""Write a value to the PLC."""
new_val = json.loads(str(value).replace("'", '"'))
tag_n = str(new_val['tag']) # "cmd_Start"
val_n = new_val['val']
w = write_tag(str(PLC_IP_ADDRESS), tag_n, val_n, plc_type="Micro800")
logger.info("Result of plcpond_writeplctag(self, {}, {}) = {}".format(name, value, w))
if w is None:
w = "Error writing to PLC..."
return w
def totalize(self, val):
right_now = dt.today()
month = right_now.month
day = right_now.day
#Totalize Today, Yesterday, Month, Last Month
#if the stored day is 0 then it's a fresh run of this should initalize values now
if TOTALIZER['Day'] == 0:
TOTALIZER['Day'] = day
TOTALIZER['Month'] = month
TOTALIZER['Daily Holding'] = val
TOTALIZER['Monthly Holding'] = val
persistence.store(TOTALIZER, 'totalizers.json')
#Communication error during initialization check if lifetime has reported properly and update holdings
if TOTALIZER['Daily Holding'] == None and not(val == None):
TOTALIZER['Daily Holding'] = val
TOTALIZER['Monthly Holding'] = val
try:
if val - TOTALIZER['Daily Holding'] - TOTALIZER['Todays'] > 500 or time.time() - TOTALIZER['Last Report'] > 3600 or self.force_send:
TOTALIZER['Todays'] = val - TOTALIZER['Daily Holding']
TOTALIZER['Current Months'] = val - TOTALIZER['Monthly Holding']
TOTALIZER['Lifetime'] = val
self.sendtodbDev(1, 'today_flow', TOTALIZER['Todays'], 0, 'plcpond')
self.sendtodbDev(1, 'current_month_flow', TOTALIZER['Current Months'], 0, 'plcpond')
self.sendtodbDev(1, 'yesterday_flow', TOTALIZER['Yesterdays'], 0, 'plcpond')
if self.force_send:
self.sendtodbDev(1, 'prev_month_flow', TOTALIZER['Previous Months'], 0, 'plcpond')
TOTALIZER['Last Report'] = time.time()
except:
if time.time() - TOTALIZER['Last Report'] > 3600 or self.force_send:
self.sendtodbDev(1, 'today_flow', TOTALIZER['Todays'], 0, 'plcpond')
self.sendtodbDev(1, 'current_month_flow', TOTALIZER['Current Months'], 0, 'plcpond')
self.sendtodbDev(1, 'yesterday_flow', TOTALIZER['Yesterdays'], 0, 'plcpond')
if self.force_send:
self.sendtodbDev(1, 'prev_month_flow', TOTALIZER['Previous Months'], 0, 'plcpond')
TOTALIZER['Last Report'] = time.time()
#If the current day doesn't equal the stored day roll the dailies over
if not(day == TOTALIZER['Day']):
#if a comms error use the stored values else use the latested values
if val == None:
TOTALIZER['Yesterdays'] = TOTALIZER['Todays']
TOTALIZER['Todays'] = 0
TOTALIZER['Daily Holding'] = TOTALIZER['Lifetime']
else:
TOTALIZER['Yesterdays'] = val - TOTALIZER['Daily Holding']
TOTALIZER['Todays'] = 0
TOTALIZER['Daily Holding'] = val
TOTALIZER['Lifetime'] = val
TOTALIZER['Day'] = day
self.sendtodbDev(1, 'today_flow', TOTALIZER['Todays'], 0, 'plcpond')
self.sendtodbDev(1, 'yesterday_flow', TOTALIZER['Yesterdays'], 0, 'plcpond')
TOTALIZER['Last Report'] = time.time()
#the day has rolled over if the month also rolls over
if not(month == TOTALIZER['Month']):
#if a comms error use the stored values else use the latested values
if val == None:
TOTALIZER['Previous Months'] = TOTALIZER['Current Months']
TOTALIZER['Current Months'] = 0
TOTALIZER['Monthly Holding'] = TOTALIZER['Lifetime']
else:
TOTALIZER['Previous Months'] = val - TOTALIZER['Monthly Holding']
TOTALIZER['Current Months'] = 0
TOTALIZER['Monthly Holding'] = val
TOTALIZER['Month'] = month
self.sendtodbDev(1, 'current_month_flow', TOTALIZER['Current Months'], 0, 'plcpond')
self.sendtodbDev(1, 'prev_month_flow', TOTALIZER['Previous Months'], 0, 'plcpond')
TOTALIZER['Last Report'] = time.time()
persistence.store(TOTALIZER, 'totalizers.json')