diff --git a/.DS_Store b/.DS_Store index 9c63b01..4e6d795 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Thingsboard Gateway/ethernetip.json b/Thingsboard Gateway/ethernetip.json new file mode 100644 index 0000000..89a7385 --- /dev/null +++ b/Thingsboard Gateway/ethernetip.json @@ -0,0 +1,42 @@ +{ + "name": "EthernetIP connector", + "publishInterval": 600, + "devices": [ + { + "name": "Big Test Boi", + "type": "plcpond", + "ip": "192.168.2.158", + "converter": "EthernetIPConverter", + "telemetry": { + "pond1Height":{"key": "pond1height"}, + "pond1Volume":{"key": "pond1volume"}, + "pond2Height":{"key": "pond2height"}, + "pond2Volume":{"key": "pond2volume"}, + "pond3Height":{"key": "pond3height"}, + "pond3Volume":{"key": "pond3volume"}, + "pond4Height":{"key": "pond4height"}, + "pond4Volume":{"key": "pond4volume"}, + "cfgNumberOfPonds":{"key": "numPonds"} + }, + "attributes": {} + }, + { + "name": "Big Test Boi", + "type": "plcpond", + "ip": "166.252.175.224", + "converter": "EthernetIPConverter", + "telemetry": { + "pond1Height":{"key": "pond1height"}, + "pond1Volume":{"key": "pond1volume"}, + "pond2Height":{"key": "pond2height"}, + "pond2Volume":{"key": "pond2volume"}, + "pond3Height":{"key": "pond3height"}, + "pond3Volume":{"key": "pond3volume"}, + "pond4Height":{"key": "pond4height"}, + "pond4Volume":{"key": "pond4volume"}, + "cfgNumberOfPonds":{"key": "numPonds"} + }, + "attributes": {} + } + ] +} diff --git a/Thingsboard Gateway/ethernetip_connector.py b/Thingsboard Gateway/ethernetip_connector.py new file mode 100644 index 0000000..90a2f55 --- /dev/null +++ b/Thingsboard Gateway/ethernetip_connector.py @@ -0,0 +1,140 @@ +from pycomm3 import LogixDriver, CommError +from datetime import datetime as dt +from time import sleep +from random import choice +from string import ascii_lowercase + +from threading import Thread +from thingsboard_gateway.connectors.connector import Connector, log +from thingsboard_gateway.tb_utility.tb_utility import TBUtility +from thingsboard_gateway.tb_utility.tb_loader import TBModuleLoader + + +class EthernetIPConnector(Thread, Connector): + def __init__(self, gateway, config, connector_type): + super().__init__() + self.statistics = {'MessagesReceived': 0, + 'MessagesSent': 0} # Dictionary, will save information about count received and sent messages. + self.__config = config # Save configuration from the configuration file. + self.__gateway = gateway # Save gateway object, we will use some gateway methods for adding devices and saving data from them. + self.setName(self.__config.get("name","%s connector " % self.get_name() + ''.join(choice(ascii_lowercase) for _ in range(5)))) # get from the configuration or create name for logs. + log.info("Starting Custom %s connector", self.get_name()) # Send message to logger + self.daemon = True # Set self thread as daemon + self.stopped = True # Service variable for check state + self.__connected = False # Service variable for check connection to device + self.__devices = {} # Dictionary with devices, will contain devices configurations, converters for devices and serial port objects + self.__load_converters(connector_type) # Call function to load converters and save it into devices dictionary + self.__connect_to_devices() # Call function for connect to devices + self._connector_type = connector_type + self.utilities = TBModuleLoader.import_module(self._connector_type, "Utilities") + log.info('Custom connector %s initialization success.', self.get_name()) # Message to logger + log.info("Devices in configuration file found: %s ", '\n'.join(device for device in self.__devices)) # Message to logger + + def __connect_to_devices(self): + for device in self.__devices: + try: + log.debug(self.__devices[device]["device_config"]["ip"]) + self.__devices[device]["logixdriver"] = LogixDriver(self.__devices[device]["device_config"]["ip"]) + except Exception as e: + log.exception(e) + else: + self.__gateway.add_device(self.__devices[device]["device_config"]["name"], {"connector": self}, self.__devices[device]["device_config"]["type"]) + self.__connected = True + + def get_name(self): + return self.name #self.__config["name"] + + def is_connected(self): + return self.__connected + + def __load_converters(self, connector_type): + devices_config = self.__config.get('devices') + try: + + if devices_config is not None: + for device_config in devices_config: + if device_config.get('converter') is not None: + log.debug(device_config) + converter = TBModuleLoader.import_module(connector_type, device_config['converter']) + self.__devices[device_config['name']] = {'converter': converter(device_config), + 'device_config': device_config} + else: + log.error('Converter configuration for the custom connector %s -- not found, please check your configuration file.', self.get_name()) + else: + log.error('Section "devices" in the configuration not found. A custom connector %s has being stopped.', self.get_name()) + self.close() + except Exception as e: + log.exception(e) + + def run(self): + try: + while True: + if int(dt.timestamp(dt.now())) % self.__config["publishInterval"] == 0: + log.debug("Publish Interval: %s" % dt.now()) + for device in self.__devices: + try: + tags = list(self.__devices[device]["device_config"]["telemetry"].keys()) + with self.__devices[device]["logixdriver"] as driver: + data_from_device = driver.read(*tags) + log.debug(data_from_device) + converted_data = self.__devices[device]['converter'].convert(self.__devices[device]['device_config'], data_from_device) + log.debug(converted_data) + self.__gateway.send_to_storage(self.get_name(), converted_data) + except CommError as ce: + log.error("Error connecting to PLC") + log.error(ce) + except Exception as e: + log.error("Error in for loop of run()") + log.error(e) + sleep(1) + except Exception as e: + log.exception(e) + def open(self): + log.info("Starting EthernetIP Connector") + self.stopped = False + self.start() + + def close(self): + pass + + def on_attributes_update(self, content): + log.info(content) + if self.__devices.get(content["device"]) is not None: + device_config = self.__devices[content["device"]].get("device_config") + if device_config is not None and device_config.get("attributeUpdates") is not None: + requests = device_config["attributeUpdates"] + for request in requests: + attribute = request.get("attributeOnThingsBoard") + log.debug(attribute) + if attribute is not None and attribute in content["data"]: + try: + value = content["data"][attribute] + str_to_send = str(request["stringToDevice"].replace("${" + attribute + "}", str(value))).encode("UTF-8") + self.__devices[content["device"]]["serial"].write(str_to_send) + log.debug("Attribute update request to device %s : %s", content["device"], str_to_send) + time.sleep(.01) + except Exception as e: + log.exception(e) + + def server_side_rpc_handler(self, content): + log.debug(content) + method_split = content["method"].split('_') + method = None + if len(method_split) > 0: + method = "_".join(method_split[1:]) + + commands = { + "ping": self.utilities.ping, + "write_plc": self.utilities.write_plc + } + + if method is not None: + #check utilities for function + log.debug(f"Looking for {method} in Utilities") + log.info(self.__devices) + if method in commands.keys(): + if method == "write_plc": + log.info(commands[method](self.__devices["Big Test Boi"]["logixdriver"],content)) + else: + log.info(commands[method](content)) + diff --git a/Thingsboard Gateway/ethernetip_converter.py b/Thingsboard Gateway/ethernetip_converter.py new file mode 100644 index 0000000..c0a355a --- /dev/null +++ b/Thingsboard Gateway/ethernetip_converter.py @@ -0,0 +1,27 @@ +from thingsboard_gateway.connectors.converter import Converter, log + + +class EthernetIPConverter(Converter): + def __init__(self, config): + self.__config = config + self.result_dict = { + 'deviceName': config.get('name', 'EthernetIPDevice'), + 'deviceType': config.get('deviceType', 'default'), + 'attributes': [], + 'telemetry': [] + } + + def convert(self, config, data): + keys = ["attributes", "telemetry"] + for k in keys: + self.result_dict[k] = [] + if self.__config.get(k) is not None and data is not None: + for datapoint in data: + if datapoint[3] == None: + try: + log.debug(config) + converted_data = {config[k][datapoint[0]]["key"]: datapoint[1]} + self.result_dict[k].append(converted_data) + except KeyError: + continue + return self.result_dict diff --git a/Thingsboard Gateway/tb_gateway.yaml b/Thingsboard Gateway/tb_gateway.yaml new file mode 100644 index 0000000..490d07e --- /dev/null +++ b/Thingsboard Gateway/tb_gateway.yaml @@ -0,0 +1,110 @@ +thingsboard: + host: hp.henrypump.cloud + port: 1883 + remoteShell: true + remoteConfiguration: false + statsSendPeriodInSeconds: 60 + minPackSendDelayMS: 0 + checkConnectorsConfigurationInSeconds: 60 + security: + accessToken: testtesttest + qos: 1 +storage: +# type: memory +# read_records_count: 100 +# max_records_count: 100000 + type: file + data_folder_path: ./data/ + max_file_count: 10 + max_read_records_count: 10 + max_records_per_file: 10000 +# type: sqlite +# data_file_path: ./data/data.db +# messages_ttl_check_in_hours: 1 +# messages_ttl_in_days: 7 +grpc: + enabled: false + serverPort: 9595 + keepaliveTimeMs: 10000 + keepaliveTimeoutMs: 5000 + keepalivePermitWithoutCalls: true + maxPingsWithoutData: 0 + minTimeBetweenPingsMs: 10000 + minPingIntervalWithoutDataMs: 5000 +connectors: + - + name: Ethernetip Connector + type: ethernetip + class: EthernetIPConnector + configuration: ethernetip.json + +# - +# name: Modbus Connector +# type: modbus +# configuration: modbus.json +# +# - +# name: Modbus Connector +# type: modbus +# configuration: modbus_serial.json +# +# - +# name: OPC-UA Connector +# type: opcua +# configuration: opcua.json +# +# - +# name: BLE Connector +# type: ble +# configuration: ble.json +# +# - +# name: REQUEST Connector +# type: request +# configuration: request.json +# +# - +# name: CAN Connector +# type: can +# configuration: can.json +# +# - +# name: BACnet Connector +# type: bacnet +# configuration: bacnet.json +# +# - +# name: ODBC Connector +# type: odbc +# configuration: odbc.json +# +# - +# name: REST Connector +# type: rest +# configuration: rest.json +# +# - +# name: SNMP Connector +# type: snmp +# configuration: snmp.json +# +# - +# name: FTP Connector +# type: ftp +# configuration: ftp.json +# +# +# ========= Customization ========== +# +# +# - +# name: Custom Serial Connector +# type: serial +# configuration: custom_serial.json +# class: CustomSerialConnector +# +# - +# name: GRPC Connector 1 +# key: auto +# type: grpc +# configuration: grpc_connector_1.json