From f436385f670f5db3cc3f39d48e9c032a425978e8 Mon Sep 17 00:00:00 2001 From: Marijn Koesen Date: Thu, 4 Jun 2020 21:08:49 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + Dockerfile | 17 ++++++ README.md | 7 +++ requirements.txt | 2 + solaredge_modbus_mqtt.py | 81 +++++++++++++++++++++++++ solaredge_modbus_mqtt/__init__.py | 57 +++++++++++++++++ solaredge_modbus_mqtt/test_SolarData.py | 81 +++++++++++++++++++++++++ 7 files changed, 247 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 requirements.txt create mode 100755 solaredge_modbus_mqtt.py create mode 100644 solaredge_modbus_mqtt/__init__.py create mode 100644 solaredge_modbus_mqtt/test_SolarData.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d35cb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.pyc diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5b661e8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3 + +RUN mkdir /app && cd /app +WORKDIR /app + +COPY requirements.txt /app +RUN pip install -r requirements.txt + +COPY solaredge_modbus_mqtt.py /app +COPY solaredge_modbus_mqtt /app/solaredge_modbus_mqtt + +#ENV MQTT_HOST +#ENV MQTT_USER +#ENV MQTT_PASS +#ENV MQTT_PORT + +CMD ./solaredge_modbus_mqtt.py \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f234250 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +### Installation + +The easiest way is to run the docker container and set it to restart always. This basically makes it a system service that will always run. + +```shell +docker run --restart=always -e MODBUS_HOST=192.168.1.1 solaredge_modbus_mqtt:latest +``` \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..44c7050 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +paho-mqtt=1.5.0 +solaredge_modbus=0.2.0 \ No newline at end of file diff --git a/solaredge_modbus_mqtt.py b/solaredge_modbus_mqtt.py new file mode 100755 index 0000000..fb8744f --- /dev/null +++ b/solaredge_modbus_mqtt.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +import argparse +import json +import os +import solaredge_modbus +import sys +import time +import paho.mqtt.client as mqtt +from solaredge_modbus_mqtt import SolarData +from pymodbus.exceptions import ConnectionException + +if __name__ == "__main__": + argparser = argparse.ArgumentParser() + argparser.add_argument("--modbus-host", type=str, default=os.environ.get('MODBUS_HOST', '127.0.0.1'), help="ModbusTCP address") + argparser.add_argument("--modbus-port", type=int, default=os.environ.get('MODBUS_PORT', 1502), help="ModbusTCP port") + argparser.add_argument("--modbus-timeout", type=int, default=os.environ.get('MODBUS_TIMEOUT', 1), help="Connection timeout") + argparser.add_argument("--modbus-unit", type=int, default=os.environ.get('MODBUS_UNIT', 1), help="Modbus unit") + + argparser.add_argument("--mqtt-host", type=str, default=os.environ.get('MQTT_HOST', '127.0.0.1'), help="MQTT Host") + argparser.add_argument("--mqtt-port", type=int, default=os.environ.get('MQTT_PORT', 1883), help="MQTT port") + argparser.add_argument("--mqtt-user", type=str, default=os.environ.get('MQTT_USER', None), help="MQTT Username") + argparser.add_argument("--mqtt-pass", type=str, default=os.environ.get('MQTT_PASS', None), help="MQTT Password") + + argparser.add_argument("--poll-interval", type=int, default=os.environ.get('POLL_INTERVAL', 3), help="Waiting time between each poll") + + args = argparser.parse_args() + + try: + mqtt = mqtt.Client(client_id="", clean_session=True, userdata=None, protocol=mqtt.MQTTv311, transport="tcp") + mqtt.username_pw_set(args.mqtt_user, args.mqtt_pass) + mqtt.connect(args.mqtt_host, args.mqtt_port, 60) + except OSError as err: + print("Cannot connect to MQTT on {0}:{1}, error: {2}".format(args.mqtt_host, args.mqtt_port, err)) + sys.exit(1) + + inverter = solaredge_modbus.Inverter( + host=args.modbus_host, + port=args.modbus_port, + timeout=args.modbus_timeout, + unit=args.modbus_unit + ) + + run = True + publishedAutoDiscovery = False + while run: + try: + solar_data = SolarData(inverter.read_all()) + except ConnectionException as err: + print("Cannot connect to SolarEdge Modbus server on {0}:{1}, error: {2}".format(args.modbus_host, args.modbus_port, err)) + sys.exit(2) + + mqtt.loop() + + if (publishedAutoDiscovery == False): + for k, v in solar_data.data.items(): + message = { + "name": "SolarEdge " + k.replace("_", " ").title(), + "unique_id": "solaredge_" + k, + "unit_of_measurement": SolarData.fields[k]['unit'] if 'unit' in SolarData.fields[k] else '', + "state_topic": "home/hal_beneden/sensor/solaredge_inverter/" + k, + "force_update": "True", + "device": { + "identifiers": ["solaredge_inverter"], + "name": "SolarEdge Inverter" + } + } + mqtt.publish( + 'test/homeassistant/sensor/solaredge_inverter/' + str(k) + '/config', + json.dumps(message), + qos=0, + retain=True + ) + + publishedAutoDiscovery = True + + + for k, v in solar_data.data.items(): + mqtt.publish('test/home/hal_beneden/sensor/solaredge_inverter/' + k, payload=str(v), qos=0, retain=False) + + time.sleep(args.poll_interval) \ No newline at end of file diff --git a/solaredge_modbus_mqtt/__init__.py b/solaredge_modbus_mqtt/__init__.py new file mode 100644 index 0000000..22f7d27 --- /dev/null +++ b/solaredge_modbus_mqtt/__init__.py @@ -0,0 +1,57 @@ +import math + +class SolarData: + + fields = { + 'model': {'field': 'c_model'}, + 'version': {'field': 'c_version'}, + 'serialnumber': {'field': 'c_serialnumber'}, + 'deviceaddress': {'field': 'c_deviceaddress'}, + 'sunspec_did': {'field': 'c_sunspec_did'}, + + 'status': {}, + 'vendor_status': {}, + + 'current': {'scaleField': 'current_scale', 'unit': 'A'}, + 'p1_current': {'scaleField': 'current_scale', 'unit': 'A'}, + 'p2_current': {'scaleField': 'current_scale', 'unit': 'A'}, + 'p3_current': {'scaleField': 'current_scale', 'unit': 'A'}, + + 'p1_voltage': {'scaleField': 'voltage_scale', 'unit': 'V'}, + 'p2_voltage': {'scaleField': 'voltage_scale', 'unit': 'V'}, + 'p3_voltage': {'scaleField': 'voltage_scale', 'unit': 'V'}, + 'p1n_voltage': {'scaleField': 'voltage_scale', 'unit': 'V'}, + 'p2n_voltage': {'scaleField': 'voltage_scale', 'unit': 'V'}, + 'p3n_voltage': {'scaleField': 'voltage_scale', 'unit': 'V'}, + + 'power_ac': {'scaleField': 'power_ac_scale', 'unit': 'W'}, + 'frequency': {'scaleField': 'frequency_scale', 'unit': 'Hz'}, + 'power_apparent': {'scaleField': 'power_apparent_scale', 'unit': 'W'}, + 'power_reactive': {'scaleField': 'power_reactive_scale', 'unit': 'W'}, + 'power_factor': {'scaleField': 'power_factor_scale', 'unit': '%'}, + 'energy_total': {'scaleField': 'energy_total_scale', 'unit': 'Wh'}, + 'current_dc': {'scaleField': 'current_dc_scale', 'unit': 'A'}, + 'voltage_dc': {'scaleField': 'voltage_dc_scale', 'unit': 'V'}, + 'power_dc': {'scaleField': 'power_dc_scale', 'unit': 'W'}, + 'temperature': {'scaleField': 'temperature_scale', 'unit': '°C'}, + } + + def __init__(self, data): + self.rawData = data + self.data = self._parseData(data) + + def _parseData(self, data): + result = {} + + for fieldName, config in self.fields.items(): + value = data[config['field']] if 'field' in config else data[fieldName] + + if 'scaleField' in config: + value = value * math.pow(10, data[config['scaleField']]) + + result[fieldName] = value + + return result + + def getModel(self): + return self.data['model'] \ No newline at end of file diff --git a/solaredge_modbus_mqtt/test_SolarData.py b/solaredge_modbus_mqtt/test_SolarData.py new file mode 100644 index 0000000..0bc90f9 --- /dev/null +++ b/solaredge_modbus_mqtt/test_SolarData.py @@ -0,0 +1,81 @@ +import unittest +from . import SolarData + +class TestStringMethods(unittest.TestCase): + def testParse(self): + solarData = SolarData({ + 'c_model': 'SE7K-ABCDEFGH4', + 'c_version': '0004.0008.0028', + 'c_serialnumber': '7F51B342', + 'c_deviceaddress': 1, + 'c_sunspec_did': 103, + 'current': 301, + 'p1_current': 99, + 'p2_current': 102, + 'p3_current': 100, + 'current_scale': -2, + 'p1_voltage': 4056, + 'p2_voltage': 4066, + 'p3_voltage': 4075, + 'p1n_voltage': 2346, + 'p2n_voltage': 2342, + 'p3n_voltage': 2351, + 'voltage_scale': -1, + 'power_ac': 5970, + 'power_ac_scale': -1, + 'frequency': 4997, + 'frequency_scale': -2, + 'power_apparent': 7133, + 'power_apparent_scale': -1, + 'power_reactive': -3890, + 'power_reactive_scale': -1, + 'power_factor': -8376, + 'power_factor_scale': -2, + 'energy_total': 788428, + 'energy_total_scale': 0, + 'current_dc': 8126, + 'current_dc_scale': -4, + 'voltage_dc': 7471, + 'voltage_dc_scale': -1, + 'power_dc': 6071, + 'power_dc_scale': -1, + 'temperature': 4717, + 'temperature_scale': -2, + 'status': 4, + 'vendor_status': 0 + }) + + self.assertEqual('SE7K-ABCDEFGH4', solarData.data['model']) + self.assertEqual('0004.0008.0028', solarData.data['version']) + self.assertEqual('7F51B342', solarData.data['serialnumber']) + self.assertEqual(1, solarData.data['deviceaddress']) + self.assertEqual(103, solarData.data['sunspec_did']) + + self.assertAlmostEqual(3.01, solarData.data['current']) + self.assertAlmostEqual(0.99, solarData.data['p1_current']) + self.assertAlmostEqual(1.02, solarData.data['p2_current']) + self.assertAlmostEqual(1.0, solarData.data['p3_current']) + + self.assertAlmostEqual(405.6, solarData.data['p1_voltage']) + self.assertAlmostEqual(406.6, solarData.data['p2_voltage']) + self.assertAlmostEqual(407.5, solarData.data['p3_voltage']) + self.assertAlmostEqual(234.6, solarData.data['p1n_voltage']) + self.assertAlmostEqual(234.2, solarData.data['p2n_voltage']) + self.assertAlmostEqual(235.1, solarData.data['p3n_voltage']) + + self.assertAlmostEqual(597.0, solarData.data['power_ac']) + self.assertAlmostEqual(49.97, solarData.data['frequency']) + self.assertAlmostEqual(713.3, solarData.data['power_apparent']) + self.assertAlmostEqual(-389.0, solarData.data['power_reactive']) + self.assertAlmostEqual(-83.76, solarData.data['power_factor']) + self.assertAlmostEqual(788428, solarData.data['energy_total']) + + self.assertAlmostEqual(0.8126, solarData.data['current_dc']) + self.assertAlmostEqual(747.1, solarData.data['voltage_dc']) + self.assertAlmostEqual(607.1, solarData.data['power_dc']) + self.assertAlmostEqual(47.17, solarData.data['temperature']) + self.assertAlmostEqual(4, solarData.data['status']) + self.assertAlmostEqual(0, solarData.data['vendor_status']) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file