"""Define Meshify channel class.""" import time from pycomm.ab_comm.clx import Driver as ClxDriver from pycomm.cip.cip_base import CommError, DataError from file_logger import filelogger as log import minimalmodbus minimalmodbus.BAUDRATE = 9600 minimalmodbus.STOPBITS = 1 TAG_DATAERROR_SLEEPTIME = 5 def binarray(intval): """Split an integer into its bits.""" bin_string = '{0:08b}'.format(intval) bin_arr = [i for i in bin_string] bin_arr.reverse() return bin_arr def read_tag(addr, tag, plc_type="CLX"): """Read a tag from the PLC.""" direct = plc_type == "Micro800" clx = ClxDriver() try: if clx.open(addr, direct_connection=direct): try: val = clx.read_tag(tag) clx.close() return val except DataError as err: clx.close() time.sleep(TAG_DATAERROR_SLEEPTIME) log.error("Data Error during readTag({}, {}): {}".format(addr, tag, err)) except CommError: # err = c.get_status() clx.close() log.error("Could not connect during readTag({}, {})".format(addr, tag)) except AttributeError as err: clx.close() log.error("AttributeError during readTag({}, {}): \n{}".format(addr, tag, err)) clx.close() return False def read_array(addr, tag, start, end, plc_type="CLX"): """Read an array from the PLC.""" direct = plc_type == "Micro800" clx = ClxDriver() if clx.open(addr, direct_connection=direct): arr_vals = [] try: for i in range(start, end): tag_w_index = tag + "[{}]".format(i) val = clx.read_tag(tag_w_index) arr_vals.append(round(val[0], 4)) if arr_vals: clx.close() return arr_vals else: log.error("No length for {}".format(addr)) clx.close() return False except Exception: log.error("Error during readArray({}, {}, {}, {})".format(addr, tag, start, end)) err = clx.get_status() clx.close() log.error(err) clx.close() def write_tag(addr, tag, val, plc_type="CLX"): """Write a tag value to the PLC.""" direct = plc_type == "Micro800" clx = ClxDriver() try: if clx.open(addr, direct_connection=direct): try: initial_val = clx.read_tag(tag) write_status = clx.write_tag(tag, val, initial_val[1]) clx.close() return write_status except DataError as err: clx_err = clx.get_status() clx.close() log.error("--\nDataError during writeTag({}, {}, {}, plc_type={}) -- {}\n{}\n".format(addr, tag, val, plc_type, err, clx_err)) except CommError as err: clx_err = clx.get_status() log.error("--\nCommError during write_tag({}, {}, {}, plc_type={})\n{}\n--".format(addr, tag, val, plc_type, err)) clx.close() return False class Channel(object): """Holds the configuration for a Meshify channel.""" def __init__(self, mesh_name, data_type, chg_threshold, guarantee_sec, map_=False, write_enabled=False): """Initialize the channel.""" self.mesh_name = mesh_name self.data_type = data_type self.last_value = None self.value = None self.last_send_time = 0 self.chg_threshold = chg_threshold self.guarantee_sec = guarantee_sec self.map_ = map_ self.write_enabled = write_enabled def __str__(self): """Create a string for the channel.""" return "{}\nvalue: {}, last_send_time: {}".format(self.mesh_name, self.value, self.last_send_time) def check(self, new_value, force_send=False): """Check to see if the new_value needs to be stored.""" send_needed = False send_reason = "" if self.data_type == 'BOOL' or self.data_type == 'STRING' or type(new_value) == str: if self.last_send_time == 0: send_needed = True send_reason = "no send time" elif self.value is None: send_needed = True send_reason = "no value" elif self.value != new_value: if self.map_: if not self.value == self.map_[new_value]: send_needed = True send_reason = "value change" else: send_needed = True send_reason = "value change" elif (time.time() - self.last_send_time) > self.guarantee_sec: send_needed = True send_reason = "guarantee sec" elif force_send: send_needed = True send_reason = "forced" else: if self.last_send_time == 0: send_needed = True send_reason = "no send time" elif self.value is None: send_needed = True send_reason = "no value" elif abs(self.value - new_value) > self.chg_threshold: send_needed = True send_reason = "change threshold" elif (time.time() - self.last_send_time) > self.guarantee_sec: send_needed = True send_reason = "guarantee sec" elif force_send: send_needed = True send_reason = "forced" if send_needed: self.last_value = self.value if self.map_: try: self.value = self.map_[new_value] except KeyError: log.error("Cannot find a map value for {} in {} for {}".format(new_value, self.map_, self.mesh_name)) self.value = new_value else: self.value = new_value self.last_send_time = time.time() log.info("Sending {} for {} - {}".format(self.value, self.mesh_name, send_reason)) return send_needed def read(self): """Read the value.""" pass def identity(sent): """Return exactly what was sent to it.""" return sent def volume_units(vunit): units = { 0: "cm cubed/s", 1: "cm cubed/min", 2: "cm cubed/h", 3: "cm cubed/d", 4: "dm cubed/s", 5: "dm cubed/min", 6: "dm cubed/h", 7: "dm cubed/d", 8: "m cubed/s", 9: "m cubed/min", 10: "m cubed/h", 11: "m cubed/d", 12: "ml/s", 13: "ml/min", 14: "ml/h", 15: "ml/d", 16: "l/s", 17: "l/min", 18: "l/h (+)", 19: "l/d", 20: "hl/s", 21: "hl/min", 22: "hl/h", 23: "hl/d", 24: "Ml/s", 25: "Ml/min", 26: "Ml/h", 27: "Ml/d", 32: "af/s", 33: "af/min", 34: "af/h", 35: "af/d", 36: "ft cubed/s", 37: "ft cubed/min", 38: "ft cubed/h", 39: "ft cubed/d", 40: "fl oz/s (us)", 41: "fl oz/min (us)", 42: "fl oz/h (us)", 43: "fl oz/d (us)", 44: "gal/s (us)", 45: "gal/min (us)", 46: "gal/h (us)", 47: "gal/d (us)", 48: "Mgal/s (us)", 49: "Mgal/min (us)", 50: "Mgal/h (us)", 51: "Mgal/d (us)", 52: "bbl/s (us;liq.)", 53: "bbl/min (us;liq.)", 54: "bbl/h (us;liq.)", 55: "bbl/d (us;liq.)", 56: "bbl/s (us;beer)", 57: "bbl/min (us;beer)", 58: "bbl/h (us;beer)", 59: "bbl/d (us;beer)", 60: "bbl/s (us;oil)", 61: "bbl/min (us;oil)", 62: "bbl/h (us;oil)", 63: "bbl/d (us;oil)", 64: "bbl/s (us;tank)", 65: "bbl/min (us;tank)", 66: "bbl/h (us;tank)", 67: "bbl/d (us;tank)", 68: "gal/s (imp)", 69: "gal/min (imp)", 70: "gal/h (imp)", 71: "gal/d (imp)", 72: "Mgal/s (imp)", 73: "Mgal/min (imp)", 74: "Mgal/h (imp)", 75: "Mgal/d (imp)", 76: "bbl/s (imp;beer)", 77: "bbl/min (imp;beer)", 78: "bbl/h (imp;beer)", 79: "bbl/d (imp;beer)", 80: "bbl/s (imp;oil)", 81: "bbl/min (imp;oil)", 82: "bbl/h (imp;oil)", 83: "bbl/d (imp;oil)", 88: "kgal/s (us)", 89: "kgal/min (us)", 90: "kgal/h (us)", 91: "kgal/d (us)", 92: "MMft cubed/s", 93: "MMft cubed/min", 94: "MMft cubed/h", 96: "Mft cubed/d" } return units[vunit] def totalizer_units(tunit): units = { 0: "cm cubed", 1: "dm cubed", 2: "m cubed", 3: "ml", 4: "l", 5: "hl", 6: "Ml Mega", 8: "af", 9: "ft cubed", 10: "fl oz (us)", 11: "gal (us)", 12: "Mgal (us)", 13: "bbl (us;liq.)", 14: "bbl (us;beer)", 15: "bbl (us;oil)", 16: "bbl (us;tank)", 17: "gal (imp)", 18: "Mgal (imp)", 19: "bbl (imp;beer)", 20: "bbl (imp;oil)", 22: "kgal (us)", 23: "Mft cubed", 50: "g", 51: "kg", 52: "t", 53: "oz", 54: "lb", 55: "STon", 100: "Nl", 101: "Nm cubed", 102: "Sm cubed", 103: "Sft cubed", 104: "Sl", 105: "Sgal (us)", 106: "Sbbl (us;liq.)", 107: "Sgal (imp)", 108: "Sbbl (us;oil)", 109: "MMSft cubed", 110: "Nhl", 251: "None" } return units[tunit] def int_to_bits(n,x): return pad_to_x([int(digit) for digit in bin(n)[2:]],x) # [2:] to chop off the "0b" part def pad_to_x(n,x): while len(n) < x: n = [0] + n return n def status_codes(n): status_array = int_to_bits(n,16) status_low = { 0: "Stopped;", 1: "Operating in Forward;", 2: "Operating in Reverse;", 3: "DC operating;" } status_mid = { 0: "", 1: "Speed searching;", 2: "Accelerating;", 3: "At constant speed;", 4: "Decelerating;", 5: "Decelerating to stop;", 6: "H/W OCS;", 7: "S/W OCS;", 8: "Dwell operating;" } status_high = { 0: "Normal state", 4: "Warning occurred", 8: "Fault occurred" } values = { 0: 8, 1: 4, 2: 2, 3: 1 } stats_low = status_array[12:] stats_mid = status_array[8:12] stats_high = status_array[:4] low = 0 mid = 0 high = 0 for x in range(4): if stats_low[x] == 1: low = low + values[x] if stats_mid[x] == 1: mid = mid + values[x] if stats_high[x] == 1: high = high + values[x] return status_low[low] + " " + status_mid[mid] + ' ' + status_high[high] def fault_code_a(n): fault_code_array = int_to_bits(n,16) """ fault = { 0: "OCT", 1: "OVT", 2: "EXT-A", 3: "EST", 4: "COL", 5: "GFT", 6: "OHT", 7: "ETH", 8: "OLT", 9: "Reserved", 10: "EXT-B", 11: "EEP", 12: "FAN", 13: "POT", 14: "IOLT", 15: "LVT" } """ fault = { 0: "Overload Trip", 1: "Underload Trip", 2: "Inverter Overload Trip", 3: "E-Thermal Trip", 4: "Ground Fault Trip", 5: "Output Image Trip", 6: "Inmput Imaging Trip", 7: "Reserved", 8: "Reserved", 9: "NTC Trip", 10: "Overcurrent Trip", 11: "Overvoltage Trip", 12: "External Trip", 13: "Arm Short", 14: "Over Heat Trip", 15: "Fuse Open Trip" } faults = [] counter = 15 for x in range(16): if fault_code_array[x] == 1: faults = [fault[counter]] + faults counter = counter - 1 return ' '.join(faults) def fault_code_b(n): fault_code_array = int_to_bits(n,8) """ fault = { 0: "COM", 1: "Reserved", 2: "NTC", 3: "REEP", 4: "OC2", 5: "NBR", 6: "SAFA", 7: "SAFB" } """ fault = { 0: "Reserved", 1: "Reserved", 2: "Reserved", 3: "FAN Trip", 4: "Reserved", 5: "Reserved", 6: "Pre PID Fail", 7: "Bad contact at basic I/O board", 8: "External Brake Trip", 9: "No Motor Trip", 10: "Bad Option Card", 11: "Reserved", 12: "Reserved", 13: "Reserved", 14: "Pre Over Heat Trip", 15: "Reserved" } faults = [] counter = 7 for x in range(8): if fault_code_array[x] == 1: faults = [fault[counter]] + faults counter = counter - 1 return ' '.join(faults) class ModbusChannel(Channel): """Modbus channel object.""" def __init__(self, mesh_name, register_number, data_type, chg_threshold, guarantee_sec, channel_size=1, map_=False, write_enabled=False, transform_fn=identity, unit_number=1, scaling=0): """Initialize the channel.""" super(ModbusChannel, self).__init__(mesh_name, data_type, chg_threshold, guarantee_sec, map_, write_enabled) self.mesh_name = mesh_name self.register_number = register_number self.channel_size = channel_size self.data_type = data_type self.last_value = None self.value = None self.last_send_time = 0 self.chg_threshold = chg_threshold self.guarantee_sec = guarantee_sec self.map_ = map_ self.write_enabled = write_enabled self.transform_fn = transform_fn self.unit_number = unit_number self.instrument = minimalmodbus.Instrument('/dev/ttyS0', self.unit_number) self.scaling= scaling def read(self): """Return the transformed read value.""" if self.data_type == "FLOAT": try: read_value = self.instrument.read_float(self.register_number,4,self.channel_size) except IOError as e: log.info(e) return None elif self.data_type == "INTEGER" or self.data_type == "STRING": try: read_value = self.instrument.read_register(self.register_number, self.scaling, 4) except IOError as e: log.info(e) return None read_value = self.transform_fn(read_value) return read_value def write(self, value): """Write a value to a register""" if self.data_type == "FLOAT": value = float(value) elif self.data_type == "INTEGER": value = int(value) else: value = str(value) try: self.instrument.write_register(self.register_number,value, self.scaling, 16 if self.channel_size > 1 else 6 ) return True except Exception as e: log.info("Failed to write value: {}".format(e)) return False class PLCChannel(Channel): """PLC Channel Object.""" def __init__(self, ip, mesh_name, plc_tag, data_type, chg_threshold, guarantee_sec, map_=False, write_enabled=False, plc_type='CLX'): """Initialize the channel.""" super(PLCChannel, self).__init__(mesh_name, data_type, chg_threshold, guarantee_sec, map_, write_enabled) self.plc_ip = ip self.mesh_name = mesh_name self.plc_tag = plc_tag self.data_type = data_type self.last_value = None self.value = None self.last_send_time = 0 self.chg_threshold = chg_threshold self.guarantee_sec = guarantee_sec self.map_ = map_ self.write_enabled = write_enabled self.plc_type = plc_type def read(self): """Read the value.""" plc_value = None if self.plc_tag and self.plc_ip: read_value = read_tag(self.plc_ip, self.plc_tag, plc_type=self.plc_type) if read_value: plc_value = read_value[0] return plc_value class BoolArrayChannels(Channel): """Hold the configuration for a set of boolean array channels.""" def __init__(self, ip, mesh_name, plc_tag, data_type, chg_threshold, guarantee_sec, map_=False, write_enabled=False): """Initialize the channel.""" super(BoolArrayChannels, self).__init__(mesh_name, data_type, chg_threshold, guarantee_sec, map_, write_enabled) self.plc_ip = ip self.mesh_name = mesh_name self.plc_tag = plc_tag self.data_type = data_type self.last_value = None self.value = None self.last_send_time = 0 self.chg_threshold = chg_threshold self.guarantee_sec = guarantee_sec self.map_ = map_ self.write_enabled = write_enabled def compare_values(self, new_val_dict): """Compare new values to old values to see if the values need storing.""" send = False for idx in new_val_dict: try: if new_val_dict[idx] != self.last_value[idx]: send = True except KeyError: log.error("Key Error in self.compare_values for index {}".format(idx)) send = True return send def read(self, force_send=False): """Read the value and check to see if needs to be stored.""" send_needed = False send_reason = "" if self.plc_tag: val = read_tag(self.plc_ip, self.plc_tag) if val: bool_arr = binarray(val[0]) new_val = {} for idx in self.map_: try: new_val[self.map_[idx]] = bool_arr[idx] except KeyError: log.error("Not able to get value for index {}".format(idx)) if self.last_send_time == 0: send_needed = True send_reason = "no send time" elif self.value is None: send_needed = True send_reason = "no value" elif self.compare_values(new_val): send_needed = True send_reason = "value change" elif (time.time() - self.last_send_time) > self.guarantee_sec: send_needed = True send_reason = "guarantee sec" elif force_send: send_needed = True send_reason = "forced" if send_needed: self.value = new_val self.last_value = self.value self.last_send_time = time.time() log.info("Sending {} for {} - {}".format(self.value, self.mesh_name, send_reason)) return send_needed