diff --git a/README.md b/README.md
new file mode 100755
index 0000000..7887f26
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# Trannergy MQTT
+MQTT client for Trannergy solar inverter (PVL5400). Written in Python 3.x
+This program might also work for Ginlong and Omnik inverters.
+The inverter has to be connected to the LAN network where this script is running.
+
+Includes Home Assistant MQTT Auto Discovery.
+## Usage:
+* Copy `systemd/trannergy-mqtt.service` to `/etc/systemd/system`
+* Adapt path in `trannergy-mqtt.service` to your install location (default: `/opt/iot/trannergy`)
+* Copy `config.rename.py` to `config.py` and adapt for your configuration (minimal: mqtt ip, username, password)
+* `sudo systemctl enable trannery-mqtt`
+* `sudo systemctl start trannergy-mqtt`
+
+Use
+http://mqtt-explorer.com/
+to test & inspect MQTT messages
+
+## Requirements
+* paho-mqtt
+* python 3.x
+
+Tested under Linux; there is no reason why it does not work under Windows.
+
+## Licence
+GPL v3
+
+## Versions
+1.2.2:
+* Initial version on github
\ No newline at end of file
diff --git a/config.py b/config.rename.py
old mode 100755
new mode 100644
similarity index 50%
rename from config.py
rename to config.rename.py
index b022edb..de2f7f3
--- a/config.py
+++ b/config.rename.py
@@ -1,35 +1,57 @@
-#!/usr/bin/python3
-
-"""
- Rename to config.py
-
- Configure:
- - MQTT client
- - Debug level
-
-"""
-
-# [ LOGLEVELS ]
-# DEBUG, INFO, WARNING, ERROR, CRITICAL
-loglevel = "INFO"
-
-# Using local dns names was not always reliable with PAHO
-MQTT_BROKER = "192.168.1.1"
-MQTT_PORT = 1883
-MQTT_CLIENT_UNIQ = 'mqtt-trannergy'
-MQTT_QOS = 1
-MQTT_USERNAME = "ijntema"
-MQTT_PASSWORD = "mosquitto0000"
-
-# Max nrof MQTT messages per second
-# Set to 0 for unlimited rate
-MQTT_RATE = 100
-
-MQTT_TOPIC_PREFIX = "solar/trannergy/roof_w"
-
-# [ InfluxDB ]
-# Add a influxdb database tag, for Telegraf processing (database:INFLUXDB)
-# This is not required for core functionality of this parser
-# Set to None if Telegraf is not used
-INFLUXDB = "solar"
-#INFLUXDB = None
\ No newline at end of file
+#!/usr/bin/python3
+
+"""
+ Rename to config.py
+
+ Configure:
+ - MQTT client
+ - Debug level
+
+"""
+
+# [ LOGLEVELS ]
+# DEBUG, INFO, WARNING, ERROR, CRITICAL
+loglevel = "INFO"
+
+# Using local dns names was not always reliable with PAHO
+MQTT_BROKER = "192.168.1.1"
+MQTT_PORT = 1883
+MQTT_CLIENT_UNIQ = 'mqtt-trannergy'
+MQTT_QOS = 1
+MQTT_USERNAME = "ijntema"
+MQTT_PASSWORD = "mosquitto0000"
+
+# Max nrof MQTT messages per second transmitted by MQTT client
+# Set to 0 for unlimited rate
+MQTT_RATE = 100
+
+MQTT_TOPIC_PREFIX = "solar/trannergy/roof_w"
+
+# [ InfluxDB ]
+# Add a influxdb database tag, for Telegraf processing (database:INFLUXDB)
+# This is not required for core functionality of this parser
+# Set to None if Telegraf is not used
+INFLUXDB = "solar"
+#INFLUXDB = None
+
+#[ TRANNERGY INVERTER ]
+INV_SERIAL = "PVL5400N177E4008"
+
+# Required when using TCPCLIENT reader
+INV_SERIAL_LOGGER = 625830567
+INV_IP = "192.168.1.53"
+INV_TCPCLIENTPORT = 8899
+
+# NROF parameter reads from inverter per hour (60 equals every minute)
+INV_READ_RATE = 60
+
+# Select one of the two available readers
+#
+# If supported: can read high frequent all inverter parameters
+INV_READER="TCPCLIENT"
+
+# Inverter sends every 5 minutes an update of all parameters
+# Configure in inverter
+# Advanced settings
+# Server: ip address to ip where this python script is running; port 3203
+#INV_READER="LISTEN"
\ No newline at end of file
diff --git a/hadiscovery.py b/hadiscovery.py
new file mode 100755
index 0000000..2f10845
--- /dev/null
+++ b/hadiscovery.py
@@ -0,0 +1,144 @@
+#!/usr/bin/python3
+
+"""
+ Send Home Assistant Auto Discovery MQTT messages
+
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+"""
+import copy
+import time
+import threading
+import json
+
+# Local imports
+import config as cfg
+
+# Logging
+import __main__
+import logging
+import os
+script = os.path.basename(__main__.__file__)
+script = os.path.splitext(script)[0]
+logger = logging.getLogger(script + "." + __name__)
+
+
+class Discovery(threading.Thread):
+
+ def __init__(self, stopper, mqtt, version):
+ """
+ class init
+
+ Keyword arguments:
+ :param threading.Event() stopper:
+ :param mqtt.mqttclient() mqtt: reference to mqtt client
+ :param str version: version of the program
+ """
+
+ logger.debug(">>")
+ super().__init__()
+ self.__stopper = stopper
+ self.__mqtt = mqtt
+ self.__version = version
+ self.__interval = 3600/cfg.HA_INTERVAL
+ self.__lastmqtt = 0
+ self.__listofjsondicts = list()
+
+
+ def __del__(self):
+ logger.debug(">>")
+
+
+ def __create_discovery_JSON(self):
+ """
+ Create the HA/MQTT Autodiscovery messages
+
+ https://www.home-assistant.io/docs/mqtt/discovery/
+ https://www.home-assistant.io/integrations/sensor/
+ https://developers.home-assistant.io/docs/core/entity/sensor/#long-term-statistics
+
+ Returns:
+ None
+ """
+ d = {}
+
+ # create device JSON
+ d["name"] = "trannergy inverter"
+ d["unique_id"] = "trannergy-device"
+ d["state_topic"] = cfg.MQTT_TOPIC_PREFIX + "/status"
+ d["icon"] = "mdi:home-automation"
+ d["device"] = {"name": "trannergy inverter",
+ "sw_version": self.__version,
+ "model": "PVL5400N/trannergy-mqtt",
+ "manufacturer": "hansij66 @github.com",
+ "identifiers": ["trannergy"]
+ }
+ self.__listofjsondicts.append( copy.deepcopy(d) )
+
+ # Create entries
+ d.clear()
+ d["unique_id"] = "p_ac1"
+ d["state_topic"] = cfg.MQTT_TOPIC_PREFIX
+ d["name"] = "Power generated L1"
+ d["unit_of_measurement"] = "W"
+ # d["state_class"] = "measurement"
+ d["value_template"] = "{{value_json.p_ac1}}"
+ d["device_class"] = "power"
+ d["icon"] = "mdi:gauge"
+ d["device"] = {"identifiers": ["trannergy"]}
+ self.__listofjsondicts.append( copy.deepcopy(d))
+
+ d.clear()
+ d["unique_id"] = "yield_total"
+ d["state_topic"] = cfg.MQTT_TOPIC_PREFIX
+ d["name"] = "EL generated"
+ d["unit_of_measurement"] = "Wh"
+ d["value_template"] = "{{value_json.yield_total}}"
+ d["device_class"] = "energy"
+ d["state_class"] = "total"
+ d["icon"] = "mdi:counter"
+ d["device"] = {"identifiers": ["trannergy"]}
+ self.__listofjsondicts.append(copy.deepcopy(d))
+
+ def run(self):
+ """
+
+ Returns:
+ None
+ """
+ logger.debug(">>")
+
+ self.__create_discovery_JSON()
+
+ # infinite loop
+ if cfg.HA_DISCOVERY:
+ while not self.__stopper.is_set():
+ # calculate time elapsed since last MQTT
+ t_elapsed = int(time.time()) - self.__lastmqtt
+
+ if t_elapsed > self.__interval:
+ for dict in self.__listofjsondicts:
+ topic = "homeassistant/sensor/" + cfg.HA_MQTT_DISCOVERY_TOPIC_PREFIX + "/" + dict["unique_id"] + "/config"
+ self.__mqtt.do_publish(topic, json.dumps(dict, separators=(',', ':')), retain=True)
+ self.__lastmqtt = int(time.time())
+ else:
+ # wait...
+ time.sleep(0.5)
+
+ # If configured, remove MQTT Auto Discovery configuration
+ if cfg.HA_DELETECONFIG:
+ for dict in self.__listofjsondicts:
+ topic = "homeassistant/sensor/" + cfg.MQTT_TOPIC_PREFIX + "/" + dict["unique_id"] + "/config"
+ self.__mqtt.do_publish(topic, "")
diff --git a/trannergy_tcpclient.py b/trannergy_tcpclient.py
new file mode 100755
index 0000000..d140421
--- /dev/null
+++ b/trannergy_tcpclient.py
@@ -0,0 +1,179 @@
+"""
+Read Trannergy telegrams via TCP Client.
+
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+"""
+
+import socket
+import threading
+import time
+import binascii
+import config as cfg
+#import inverter_msg as msg
+
+# Logging
+import __main__
+import logging
+import os
+script = os.path.basename(__main__.__file__)
+script = os.path.splitext(script)[0]
+logger = logging.getLogger(script + "." + __name__)
+
+
+class TaskReadSerial(threading.Thread):
+
+ def __init__(self, trigger, stopper, telegram):
+ """
+ Args:
+ :param threading.Event() trigger: signals that new telegram is available
+ :param threading.Event() stopper: stops thread
+ :param list() telegram: trannergy telegram
+ """
+
+ logger.debug(">>")
+ super().__init__()
+ self.__trigger = trigger
+ self.__stopper = stopper
+ self.__telegram = telegram
+ self.__counter = 0
+ self.__lastread = 0
+ self.__interval = 3600/cfg.INV_READ_RATE
+ self.__requestmsg = self.__request_string(cfg.INV_SERIAL_LOGGER)
+ self.__sock = None
+
+ def __del__(self):
+ logger.debug(">>")
+
+ def __socket_connect(self):
+ """
+ Create socket/connection to inverter
+ """
+
+ while not self.__stopper.is_set():
+ try:
+ self.__sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.__sock.connect((cfg.INV_IP, cfg.INV_TCPCLIENTPORT))
+ return
+ except Exception as e:
+ logger.info(f"Read SOCKS: {type(e).__name__}: {str(e)}")
+ time.sleep(30)
+
+ def __request_string(self, ser):
+ """
+ Reuse from
+ https://github.com/jbouwh/omnikdatalogger/blob/dev/apps/omnikdatalogger/omnik/InverterMsg.py
+ The request string is build from several parts. The first part is a
+ fixed 4 char string; the second part is the reversed hex notation of
+ the Wi-Fi logger s/n twice; then again a fixed string of two chars; a checksum of
+ the double s/n with an offset; and finally a fixed ending char.
+
+ Args:
+ :param str ser: s/n inverter wifi module
+ """
+ responseString = b"\x68\x02\x40\x30"
+
+ doublehex = hex(ser)[2:] * 2
+ hexlist = [
+ bytes.fromhex(doublehex[i: i + 2])
+ for i in reversed(range(0, len(doublehex), 2))
+ ]
+
+ cs_count = 115 + sum([ord(c) for c in hexlist])
+ cs = bytes.fromhex(hex(cs_count)[-2:])
+ responseString += b"".join(hexlist) + b"".join([b"\x01\x00", cs, b"\x16"])
+ return responseString
+
+ def __read_serial(self):
+ """
+ Reads Trannergy telegrams; stores in global variable (self.__telegram)
+ Sets threading event to signal other clients (parser) that
+ new telegram is available.
+
+ :raises Exceptions
+ """
+ logger.debug(">>")
+
+ while not self.__stopper.is_set():
+
+ # wait till parser has copied telegram content
+ # ...we need the opposite of trigger.wait()...block when set; not available
+ while self.__trigger.is_set():
+ time.sleep(0.5)
+
+ self.__telegram.clear()
+
+ # Sending request message to Inverter
+ self.__sock.sendall(self.__requestmsg)
+
+ # Wait for response (listen and block thread)
+ rawdata = self.__sock.recv(1024)
+
+ serial = str(rawdata[15:31], encoding="UTF-8")
+
+ logger.debug(f"SERIAL={serial}")
+ if serial != cfg.INV_SERIAL:
+ logger.debug(f"INCORRECT MESSAGE={serial}")
+ # try again...start over....
+ continue
+
+ # convert to more readable
+ hexdata = binascii.hexlify(rawdata)
+ #hexdata = str(binascii.b2a_base64(rawmsg))
+
+ ## Often, a zero length message is received
+ ## A valid payload is 206 bytes
+ #if len(hexdata) >= 206:
+ # convert to more readable & add to telegram
+ logger.debug(f"hexdata = {hexdata}")
+
+ self.__counter += 1
+
+ # add a counter as first field to the list
+ self.__telegram.append(f"{self.__counter}")
+ self.__telegram.append(f"{hexdata}")
+
+ # Trigger that new telegram is available for MQTT
+ logger.debug("Set trigger")
+ self.__trigger.set()
+
+ while not self.__stopper.is_set():
+ t_elapsed = int(time.time()) - self.__lastread
+ if t_elapsed > self.__interval:
+ self.__lastread = int(time.time())
+ break
+ else:
+ # wait...
+ time.sleep(1)
+
+ logger.debug("<<")
+
+ def run(self):
+ logger.debug(">>")
+
+ self.__socket_connect()
+
+ while not self.__stopper.is_set():
+ try:
+ self.__read_serial()
+ except Exception as e:
+ logger.warning(f"Exception {e}")
+ time.sleep(10)
+ self.__sock.close()
+ self.__socket_connect()
+
+ self.__sock.close()
+
+ logger.debug("<<")