From d949d9375eb2d07f389b8bfc6b1cf2d84c02fbec Mon Sep 17 00:00:00 2001 From: reserve85 <111107925+reserve85@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:01:31 +0200 Subject: [PATCH 1/4] support DDSU666 Smartmeter ## V1.92 ### script * support DDSU666 Smartmeter Module (Modbus) ### config * add section `DDSU666` * add `SELECT_POWERMETER`: `USE_DDSU666` --- CHANGELOG.md | 7 +++++ GetPowerFromDDSU666.py | 59 +++++++++++++++++++++++++++++++++++ HoymilesZeroExport.py | 16 +++++++++- HoymilesZeroExport_Config.ini | 7 ++++- 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 GetPowerFromDDSU666.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a847f4..422ef07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## V1.92 +### script +* support DDSU666 Smartmeter Module (Modbus) +### config +* add section `DDSU666` +* add `SELECT_POWERMETER`: `USE_DDSU666` + ## V1.91 ### script * support Home Assistant over HTTPS (https://github.com/reserve85/HoymilesZeroExport/issues/178) diff --git a/GetPowerFromDDSU666.py b/GetPowerFromDDSU666.py new file mode 100644 index 0000000..76aeecd --- /dev/null +++ b/GetPowerFromDDSU666.py @@ -0,0 +1,59 @@ +# Script to read powermeter values from a DDSU Hoymiles compatible +# Needs "pymodbus" (it is a full Modbus protocol) to be installed, e.g. "pip install pymodbus" +# Usage: GetPowerFromDDSU666 + +from pymodbus.client import ModbusSerialClient as ModbusClient +import struct +import sys +import time + +# Function to read registers with retry mechanism +def read_registers_with_retry(client, register_address, num_registers, unit): + max_retries = 3 + retries = 0 + while retries < max_retries: + result = client.read_input_registers(register_address, num_registers, unit=unit) + if not result.isError(): + return result + else: + print("Error reading registers. Retrying...") + retries += 1 + time.sleep(1) # Wait for 1 second before retrying + print("Failed to read registers after {} retries.".format(max_retries)) + return None + +def GetPower(device_address): + # Modbus RTU Client configuration + client = ModbusClient(method='rtu', port='/dev/ttyUSB0', baudrate=9600, timeout=1) + + # Register number to be read (0x2004) which is + register_address = 0x2004 # DDSU666_POWER + + # Number of registers to be read (2 for a float) + num_registers = 2 + + # Open the connection + client.connect() + + # Read the register + result = read_registers_with_retry(client, register_address, num_registers, int(device_address)) + + # If reading was successful, continue processing + if result is not None: + # Combine the 2 registers into one 32-bit value + combined_value = (result.registers[0] << 16) + result.registers[1] + + # Interpret value as a float * 1000 because of KW value returned + interpreted_value = struct.unpack('>f', struct.pack('>I', combined_value))[0] * 1000 + + # Invert the sign if interpreted_value + interpreted_value *= -1 + + # Convert interpreted_value to an integer to display only the numbers before the decimal point + AC_GRID = int(interpreted_value) + + # Print interpreted value for debugging purpose + #print("Interpreted value of register 0x2004 for DDSU666:", AC_GRID, "W") + + # Close the connection + client.close() diff --git a/HoymilesZeroExport.py b/HoymilesZeroExport.py index 4e47f29..63cfa96 100644 --- a/HoymilesZeroExport.py +++ b/HoymilesZeroExport.py @@ -15,7 +15,7 @@ # along with this program. If not, see . __author__ = "Tobias Kraft" -__version__ = "1.91" +__version__ = "1.92" import requests import time @@ -1117,6 +1117,16 @@ def __init__(self, file: str, ip: str, user: str, password: str): def GetPowermeterWatts(self): power = subprocess.check_output([self.file, self.ip, self.user, self.password]) return CastToInt(power) + +class DDSU666(Powermeter): + def __init__(self, device_address: str): + self.file = 'GetPowerFromDDSU666.py' + self.device_address = device_address + + def GetPowermeterWatts(self): + import GetPowerFromDDSU666 + power = GetPowerFromDDSU666.GetPower(int(self.device_address)) + return CastToInt(power) def CreatePowermeter() -> Powermeter: @@ -1187,6 +1197,10 @@ def CreatePowermeter() -> Powermeter: config.get('SCRIPT', 'SCRIPT_USER'), config.get('SCRIPT', 'SCRIPT_PASS') ) + elif config.getboolean('SELECT_POWERMETER', 'USE_DDSU666'): + return DDSU666( + config.get('DDSU666', 'DEVICE_ADDRESS') + ) else: raise Exception("Error: no powermeter defined!") diff --git a/HoymilesZeroExport_Config.ini b/HoymilesZeroExport_Config.ini index 0cf81a1..cbb7ac0 100644 --- a/HoymilesZeroExport_Config.ini +++ b/HoymilesZeroExport_Config.ini @@ -19,7 +19,7 @@ # --------------------------------------------------------------------- [VERSION] -VERSION = 1.91 +VERSION = 1.92 [SELECT_DTU] # --- define your DTU (only one) --- @@ -38,6 +38,7 @@ USE_IOBROKER = false USE_HOMEASSISTANT = false USE_VZLOGGER = false USE_SCRIPT = false +USE_DDSU666 = false [AHOY_DTU] # --- defines for AHOY-DTU --- @@ -136,6 +137,10 @@ SCRIPT_FILE = GetPowerFromVictronMultiplus.sh SCRIPT_USER = SCRIPT_PASS = +[DDSU666] +# --- defines for DDSU666 Smartmeter Modul, you need to install pymodbus ("pip install pymodbus") --- +DEVICE_ADDRESS = 1 + [SELECT_INTERMEDIATE_METER] # if you have an intermediate meter ("Zwischenzähler") to measure the outputpower of your inverter you can set it here. It is faster than the DTU current_power value # --- define your intermediate meter - if you don´t have one set the following defines to false to use the value from your DTU--- From c5052c93d81e9c6c05f6ffd0ced45f338e5303e7 Mon Sep 17 00:00:00 2001 From: Wind06 <64360875+Wind06@users.noreply.github.com> Date: Wed, 1 May 2024 11:36:07 +0200 Subject: [PATCH 2/4] Create GetPowerFromDDSU666.sh Script to get AC_GRID from DDSU666 on a Raspberry Pi 4 with a Waveshare Industrial USB to RS485. --- GetPowerFromDDSU666.sh | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 GetPowerFromDDSU666.sh diff --git a/GetPowerFromDDSU666.sh b/GetPowerFromDDSU666.sh new file mode 100644 index 0000000..0cf82e2 --- /dev/null +++ b/GetPowerFromDDSU666.sh @@ -0,0 +1,53 @@ +#!/bin/sh + +# Script to read powermeter values from a DDSU666 Monophase +# Needs "mbpoll" (command line utility to communicate with ModBus slave) to be installed, e.g. "apt install mbpoll" +# Usage: GetPowerFromDDSU666 + +# read registers 8196 via ModbusTCP + +VE_SYSTEM=$(mbpoll -a "$1" -b 9600 -P none -B -t 4:float -0 -r 8196 -c 1 -1 /dev/ttyUSB0 | grep "\[.*\]:") +if [ $? -ne 0 ]; then + # failed, one more try + sleep 1 + VE_SYSTEM=$(mbpoll -a "$1" -b 9600 -P none -B -t 4:float -0 -r 8196 -c 1 -1 /dev/ttyUSB0 | grep "\[.*\]:") + if [ $? -ne 0 ]; then + type mbpoll > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo "$0: mbpoll must be installed!" + else + echo 0 + fi + exit 1 + fi +fi + +# Get AC_GRID Value in KW +AC_GRID=$(echo "$VE_SYSTEM" | sed -n -e "s/.*\[8196\]:[[:space:]]*\(-*[0-9]*\.[0-9]*\).*/\1/p") + +# Check that AC_GRID is not empty meaning that the GRID return 0 +if [ -z "$AC_GRID" ]; then + AC_GRID=$(echo "$VE_SYSTEM" | sed 's/.*: *\(.*\)$/\1/') +else +# Convert KW in W + AC_GRID=$(echo "$AC_GRID * 1000" | bc) +fi + +# Keep the integer part only +AC_GRID=$(echo "$AC_GRID" | cut -d'.' -f1) + +# Check if AC_GRID is a number +if echo "$AC_GRID" | grep -qE '^-*[0-9]+(\.[0-9]+)?$'; then + # DSSU666 Hoymiles specific if AC_GRID is positive set it as a Negative value because solar inverter are OverProduction + if [ "$(echo "$AC_GRID >= 0" | bc -l)" -eq 1 ]; then + AC_GRID=$(echo "$AC_GRID" | awk '{print ($1 < 0) ? -$1 : -$1}') + else + # DSSU666 Hoymiles specific AC_GRID is negative set it as a Positive value solar inverter are notProducing enough + AC_GRID=$(echo "$AC_GRID" | awk '{print ($1 < 0) ? -$1 : $1}') + fi +else + echo "AC_GRID is not a valid number." +fi + +echo "$AC_GRID" + From 5849243880112e75a831e68aba1c73c0daacfbb7 Mon Sep 17 00:00:00 2001 From: reserve85 <111107925+reserve85@users.noreply.github.com> Date: Thu, 2 May 2024 07:52:22 +0200 Subject: [PATCH 3/4] Delete GetPowerFromDDSU666.py --- GetPowerFromDDSU666.py | 59 ------------------------------------------ 1 file changed, 59 deletions(-) delete mode 100644 GetPowerFromDDSU666.py diff --git a/GetPowerFromDDSU666.py b/GetPowerFromDDSU666.py deleted file mode 100644 index 76aeecd..0000000 --- a/GetPowerFromDDSU666.py +++ /dev/null @@ -1,59 +0,0 @@ -# Script to read powermeter values from a DDSU Hoymiles compatible -# Needs "pymodbus" (it is a full Modbus protocol) to be installed, e.g. "pip install pymodbus" -# Usage: GetPowerFromDDSU666 - -from pymodbus.client import ModbusSerialClient as ModbusClient -import struct -import sys -import time - -# Function to read registers with retry mechanism -def read_registers_with_retry(client, register_address, num_registers, unit): - max_retries = 3 - retries = 0 - while retries < max_retries: - result = client.read_input_registers(register_address, num_registers, unit=unit) - if not result.isError(): - return result - else: - print("Error reading registers. Retrying...") - retries += 1 - time.sleep(1) # Wait for 1 second before retrying - print("Failed to read registers after {} retries.".format(max_retries)) - return None - -def GetPower(device_address): - # Modbus RTU Client configuration - client = ModbusClient(method='rtu', port='/dev/ttyUSB0', baudrate=9600, timeout=1) - - # Register number to be read (0x2004) which is - register_address = 0x2004 # DDSU666_POWER - - # Number of registers to be read (2 for a float) - num_registers = 2 - - # Open the connection - client.connect() - - # Read the register - result = read_registers_with_retry(client, register_address, num_registers, int(device_address)) - - # If reading was successful, continue processing - if result is not None: - # Combine the 2 registers into one 32-bit value - combined_value = (result.registers[0] << 16) + result.registers[1] - - # Interpret value as a float * 1000 because of KW value returned - interpreted_value = struct.unpack('>f', struct.pack('>I', combined_value))[0] * 1000 - - # Invert the sign if interpreted_value - interpreted_value *= -1 - - # Convert interpreted_value to an integer to display only the numbers before the decimal point - AC_GRID = int(interpreted_value) - - # Print interpreted value for debugging purpose - #print("Interpreted value of register 0x2004 for DDSU666:", AC_GRID, "W") - - # Close the connection - client.close() From e4aa15b2845248492874e2dfa3ab534f4d78ed80 Mon Sep 17 00:00:00 2001 From: reserve85 Date: Thu, 2 May 2024 07:56:42 +0200 Subject: [PATCH 4/4] Revert "support DDSU666 Smartmeter" This reverts commit d949d9375eb2d07f389b8bfc6b1cf2d84c02fbec. --- CHANGELOG.md | 7 ------- HoymilesZeroExport.py | 16 +--------------- HoymilesZeroExport_Config.ini | 7 +------ 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 422ef07..6a847f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,5 @@ # Changelog -## V1.92 -### script -* support DDSU666 Smartmeter Module (Modbus) -### config -* add section `DDSU666` -* add `SELECT_POWERMETER`: `USE_DDSU666` - ## V1.91 ### script * support Home Assistant over HTTPS (https://github.com/reserve85/HoymilesZeroExport/issues/178) diff --git a/HoymilesZeroExport.py b/HoymilesZeroExport.py index 63cfa96..4e47f29 100644 --- a/HoymilesZeroExport.py +++ b/HoymilesZeroExport.py @@ -15,7 +15,7 @@ # along with this program. If not, see . __author__ = "Tobias Kraft" -__version__ = "1.92" +__version__ = "1.91" import requests import time @@ -1117,16 +1117,6 @@ def __init__(self, file: str, ip: str, user: str, password: str): def GetPowermeterWatts(self): power = subprocess.check_output([self.file, self.ip, self.user, self.password]) return CastToInt(power) - -class DDSU666(Powermeter): - def __init__(self, device_address: str): - self.file = 'GetPowerFromDDSU666.py' - self.device_address = device_address - - def GetPowermeterWatts(self): - import GetPowerFromDDSU666 - power = GetPowerFromDDSU666.GetPower(int(self.device_address)) - return CastToInt(power) def CreatePowermeter() -> Powermeter: @@ -1197,10 +1187,6 @@ def CreatePowermeter() -> Powermeter: config.get('SCRIPT', 'SCRIPT_USER'), config.get('SCRIPT', 'SCRIPT_PASS') ) - elif config.getboolean('SELECT_POWERMETER', 'USE_DDSU666'): - return DDSU666( - config.get('DDSU666', 'DEVICE_ADDRESS') - ) else: raise Exception("Error: no powermeter defined!") diff --git a/HoymilesZeroExport_Config.ini b/HoymilesZeroExport_Config.ini index cbb7ac0..0cf81a1 100644 --- a/HoymilesZeroExport_Config.ini +++ b/HoymilesZeroExport_Config.ini @@ -19,7 +19,7 @@ # --------------------------------------------------------------------- [VERSION] -VERSION = 1.92 +VERSION = 1.91 [SELECT_DTU] # --- define your DTU (only one) --- @@ -38,7 +38,6 @@ USE_IOBROKER = false USE_HOMEASSISTANT = false USE_VZLOGGER = false USE_SCRIPT = false -USE_DDSU666 = false [AHOY_DTU] # --- defines for AHOY-DTU --- @@ -137,10 +136,6 @@ SCRIPT_FILE = GetPowerFromVictronMultiplus.sh SCRIPT_USER = SCRIPT_PASS = -[DDSU666] -# --- defines for DDSU666 Smartmeter Modul, you need to install pymodbus ("pip install pymodbus") --- -DEVICE_ADDRESS = 1 - [SELECT_INTERMEDIATE_METER] # if you have an intermediate meter ("Zwischenzähler") to measure the outputpower of your inverter you can set it here. It is faster than the DTU current_power value # --- define your intermediate meter - if you don´t have one set the following defines to false to use the value from your DTU---