Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions Inputs/DTSU666Modbus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from pymodbus.client.sync import ModbusSerialClient
from pymodbus.exceptions import ModbusException
from struct import unpack

def float32(result, base, addr):
low = result.getRegister(addr - base)
high = result.getRegister(addr - base + 1)
data = bytearray(4)
data[0] = high & 0xff
data[1] = high >> 8
data[2] = low & 0xff
data[3] = low >> 8

val = unpack('f', bytes(data))

return val[0]

class DTSU666Modbus(object):
# Class to interface with the DTSU666 energy meter via Modbus RTU

def __init__(self, port, baudrate=9600, parity='N', stopbits=1, timeout=3):
self.port = port
# Initialize Modbus serial client with default parameters for DTSU666
self.client = ModbusSerialClient(method="rtu", port=port, baudrate=baudrate,
parity=parity, stopbits=stopbits, timeout=timeout)

def fetch(self, completionCallback):
try:
# Reading first block of registers
base = 0x2000
result = self.client.read_input_registers(base, 0x52, unit=1)
if isinstance(result, ModbusException):
print("Exception from DTSU666: {}".format(result))
return

# Parsing registers from the first block
self.vals = {}
self.vals['name'] = self.port.replace("/dev/tty", "");
self.vals['Line 1 to Line 2 volts'] = float32(result, base, 0x2000) / 10;
self.vals['Line 2 to Line 3 volts'] = float32(result, base, 0x2002) / 10;
self.vals['Line 3 to Line 1 volts'] = float32(result, base, 0x2004) / 10;
self.vals['Phase 1 line to neutral volts'] = float32(result, base, 0x2006) / 10;
self.vals['Phase 2 line to neutral volts'] = float32(result, base, 0x2008) / 10;
self.vals['Phase 3 line to neutral volts'] = float32(result, base, 0x200A) / 10;
self.vals['Phase 1 current'] = float32(result, base, 0x200C) / 1000;
self.vals['Phase 2 current'] = float32(result, base, 0x200E) / 1000;
self.vals['Phase 3 current'] = float32(result, base, 0x2010) / 1000;
self.vals['Phase 1 power'] = float32(result, base, 0x2014) / 10;
self.vals['Phase 2 power'] = float32(result, base, 0x2016) / 10;
self.vals['Phase 3 power'] = float32(result, base, 0x2018) / 10;
self.vals['Phase 1 volt amps reactive'] = float32(result, base, 0x201C) / 10;
self.vals['Phase 2 volt amps reactive'] = float32(result, base, 0x201E) / 10;
self.vals['Phase 3 volt amps reactive'] = float32(result, base, 0x2020) / 10;
self.vals['Phase 1 power factor'] = float32(result, base, 0x202C) / 1000;
self.vals['Phase 2 power factor'] = float32(result, base, 0x202E) / 1000;
self.vals['Phase 3 power factor'] = float32(result, base, 0x2030) / 1000;
self.vals['Total system power'] = float32(result, base, 0x2012) / 10;
self.vals['Total system VAr'] = float32(result, base, 0x201A) / 10;
self.vals['Total system power factor'] = float32(result, base, 0x202A) / 1000;
self.vals['Frequency Of supply voltages'] = float32(result, base, 0x2044) / 100;
self.vals['Total system power demand'] = float32(result, base, 0x2044) / 10;

# Reading second block of registers
base = 0x401E
result = self.client.read_input_registers(base, 52, unit=1)
if isinstance(result, ModbusException):
print("Exception from DTSU666: {}".format(result))
return

# Parsing registers from the second block
# Adjusting the base address and register address accordingly
self.vals['Total import kWh'] = float32(result, base, 0x401E) * 1000;
self.vals['Total export kWh'] = float32(result, base, 0x4028) * 1000;
self.vals['Total Q1 kvarh'] = float32(result, base, 0x4032) * 1000;
self.vals['Total Q2 kvarh'] = float32(result, base, 0x403C) * 1000;
self.vals['Total Q3 kvarh'] = float32(result, base, 0x4046) * 1000;
self.vals['Total Q4 kvarh'] = float32(result, base, 0x4050) * 1000;

completionCallback(self.vals, None)

except ModbusException as e:
print(f"Modbus Error: {e}")
return None
6 changes: 6 additions & 0 deletions Inputs/SolaxXHybridModbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ def solaxRegisterCCallback(self, result):
def wakeupInverter(self, result):
result = self.factory.getClient().write_register(0x56, 1)

def tickleRemoteControl(self, result):
result = self.factory.getClient().write_register(0x51, 1)

def chargeBattery(self, power):
self.requestedBatteryPower = power

Expand All @@ -337,6 +340,9 @@ def chargeBattery(self, power):
# else:
# result = self.factory.getClient().write_registers(0x07C, [1, power, 0, 0, 0])
result = self.factory.getClient().write_register(0x52, power)
inverter = self.config['Solax-BatteryControl']['Inverter'][self.vals['name']]
if 'tickle-remote-control' in inverter and inverter['tickle-remote-control']:
result.addCallback(self.tickleRemoteControl)
# if power != 0:
# result.addCallback(self.wakeupInverter)

Expand Down
30 changes: 30 additions & 0 deletions Outputs/Mqtt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from paho.mqtt import client as mqtt

class Mqtt(object):
def __init__(self, config):
self.mqtt_host = config['mqtt_host']
self.mqtt_port = config['mqtt_port']
self.mqtt_keepalive = config['mqtt_keepalive']
self.mqtt_user = config['mqtt_user']
self.mqtt_pass = config['mqtt_pass']
self.mqtt_topic = config['mqtt_topic']

def on_connect(client, userdata, flags, rc):
if rc != 0:
print("Failed to connect, return code %d\n", rc)

def send(self, vals):
client = mqtt.Client()
client.username_pw_set(username=self.mqtt_user, password=self.mqtt_pass)
client.connect(self.mqtt_host, self.mqtt_port, self.mqtt_keepalive)

inverterDetails = vals.copy()
inverterDetails.pop('Serial', None)
inverterDetails.pop('#SolaxClient', None)

for x, y in inverterDetails.items():
client.publish(f"{self.mqtt_topic}/{x}", y)
# print(f"Publish topic:{self.mqtt_topic}/{x} - Value:{y}")

client.disconnect()

12 changes: 9 additions & 3 deletions Outputs/SolaxBatteryControl.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ def send(self, vals, batteryAPI):
if 'name' not in inverter:
inverter['name'] = inverterName

if 'use-total-power' not in inverter:
inverter['use-total-power'] = False

inverter['Battery Capacity'] = vals['Battery Capacity']

phase = inverter['phase']
Expand Down Expand Up @@ -152,9 +155,12 @@ def send(self, vals, batteryAPI):
self.assistNeeded[inverterName] = True
return

# Try and zero our phase power
#print("Initial discharge power is {}, additional from phase is {}\n".format(inverter['DischargePower'], self.phasePower[phase]))
inverter['DischargePower'] += self.phasePower[phase] * 0.25
if inverter['use-total-power']:
inverter['DischargePower'] += self.totalPower * 0.25
else:
# Try and zero our phase power
#print("Initial discharge power is {}, additional from phase is {}\n".format(inverter['DischargePower'], self.phasePower[phase]))
inverter['DischargePower'] += self.phasePower[phase] * 0.25

if self.assistNeeded[inverterName]:
if inverter['DischargePower'] >= 0 and inverter['DischargePower'] < inverter['single-phase-discharge-limit'] / len(self.config['Inverter']):
Expand Down
13 changes: 13 additions & 0 deletions PowerScraper.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[Unit]
Description=Power Scraper

[Service]
Type=simple
User=root
WorkingDirectory=/usr/local/PowerScraper
ExecStart=/usr/local/PowerScraper/power_scraper.py
Restart=on-abort

[Install]
WantedBy=multi-user.target

19 changes: 19 additions & 0 deletions config-sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ inverters = ['solax-x1-hybrid.lan']
#stopbits = 1
#ports = ["/dev/ttyMainsMeter"]

# Enable this section to scrape Modbus/RTU (RS485) connected DTSU666 energy meters
#[DTSU666]
#poll_period = 1 # seconds
#timeout = 1 # seconds
#baud = 9600
#parity = 'N'
#stopbits = 1
#ports = ["/dev/ttyMainsMeter"]

# Enable this section to scrape Modbus/RTU (RS485, "meter" connection in the manual) for Solax X3 inverters
#[SolaxX3RS485]
#poll_period = 10 # seconds
Expand All @@ -53,6 +62,15 @@ inverters = ['solax-x1-hybrid.lan']
#influx_pass = "influxpassword"
#influx_retention_policy = 'autogen'

# Enable this sction to output to MQTT
#[mqtt]
#mqtt_host = "localhost"
#mqtt_port = 1883
#mqtt_keepalive = 60
#mqtt_user = "mqttuser"
#mqtt_pass = "mqttpassword"
#mqtt_topic = "/yourtopic"

# Enable these sections to control battery charge/discharge on Solax SK-SU5000E inverters
# This does a number of things:
# 1. Allows more than 2 time periods through the day
Expand All @@ -71,6 +89,7 @@ inverters = ['solax-x1-hybrid.lan']
# Defines the inverters that will participate
#[Solax-BatteryControl.Inverter.solax1] # First inverter
#phase = 1 # Which phase the inverter is connected to, as seen by the power consumption meter
#use-total-power = false # If true, the inverter will try to zero the total power, rather than the phase power
#single-phase-charge-limit = 1000 # If the charge rate is below this, aim to zero our own phase, if it's above, aim to zero total power
#single-phase-discharge-limit = 1000 # If the discharge rate is below this, aim to zero our own phase, if it's above, aim to zero total power
#max-charge = 2000 # Maximum battery charge rate in Watts
Expand Down
19 changes: 18 additions & 1 deletion power_scraper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Scrapes Inverter information from solax inverters and presents it to OpenEnergyMonitor
#
# Setup:
# pip3 install toml twisted pymodbus influxdb_client
# pip3 install toml twisted pymodbus influxdb_client paho-mqtt
# cp config-example.toml config.toml
# vi config.toml
#
Expand Down Expand Up @@ -38,10 +38,12 @@
from Inputs.SolaxModbus import SolaxModbus
from Inputs.SolaxXHybridModbus import SolaxXHybridModbus
from Inputs.SDM630ModbusV2 import SDM630ModbusV2
from Inputs.DTSU666Modbus import DTSU666Modbus
from Inputs.SolaxX3RS485 import SolaxX3RS485
from Outputs.SolaxBatteryControl import SolaxBatteryControl
from Outputs.EmonCMS import EmonCMS
from Outputs.Influx2 import Influx2
from Outputs.Mqtt import Mqtt

from twisted.internet.defer import setDebugging
setDebugging(True)
Expand Down Expand Up @@ -95,6 +97,10 @@ def shutdown():
print("Setting up Influx")
outputs.append(Influx2(config['influx']))

if 'mqtt' in config:
print("Setting up Mqtt")
outputs.append(Mqtt(config['mqtt']))

if 'Solax-BatteryControl' in config:
print("Setting up Solax-BatteryControl")
outputs.append(SolaxBatteryControl(config['Solax-BatteryControl']))
Expand Down Expand Up @@ -140,6 +146,17 @@ def shutdown():
looperSDM630 = task.LoopingCall(inputActions, SDM630Meters)
looperSDM630.start(config['SDM630ModbusV2']['poll_period'])

if 'DTSU666' in config:
print("Setting up DTSU666")
DTSU666Meters = []
for meter in config['DTSU666']['ports']:
modbusMeter = DTSU666Modbus(meter, config['DTSU666']['baud'], config['DTSU666']['parity'],
config['DTSU666']['stopbits'], config['DTSU666']['timeout'])
DTSU666Meters.append(modbusMeter)

looperDTSU666 = task.LoopingCall(inputActions, DTSU666Meters)
looperDTSU666.start(config['DTSU666']['poll_period'])

if 'SolaxX3RS485' in config:
print("Setting up SolaxX3RS485")
SolaxRS485Meters = []
Expand Down
5 changes: 5 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pymodbus==2.5.3
influxdb_client
toml
twisted