Initial commit
This commit is contained in:
7 files changed
+247
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
+17
@@ -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
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
paho-mqtt=1.5.0
|
||||||
|
solaredge_modbus=0.2.0
|
||||||
Executable
+81
@@ -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)
|
||||||
@@ -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']
|
||||||
@@ -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()
|
||||||
Reference in new issue
Block a user