first test code
This commit is contained in:
parent
75f28c9b08
commit
0ec212f0b4
1 changed files with 269 additions and 0 deletions
269
froniustest.py
Normal file
269
froniustest.py
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
# Copyright notes: this script was initially made public by user MobusTest in photovoltaikforum.com
|
||||||
|
# https://www.photovoltaikforum.com/thread/117173-datamanager-und-modbus-register-mit-modbus-tcp/?postID=2021510#post2021510
|
||||||
|
# This client re-uses all the modbus protocoll implementation and data conversion from the original work.
|
||||||
|
# The client provides an easy way to explore modbus registers on a Fronius inverter
|
||||||
|
# This software is provided "as is" without any guarantee
|
||||||
|
|
||||||
|
# Subsequently this was modified to implement a target charging mode
|
||||||
|
#
|
||||||
|
# The idea is that at some specified time before a peak pricing period
|
||||||
|
# this script is run to see if the system can make it through the peak
|
||||||
|
# period on battery alone.
|
||||||
|
#
|
||||||
|
# This involes
|
||||||
|
# * obtaining the curent SOC
|
||||||
|
# * obtaining the last hour's discharge rate
|
||||||
|
# * calculating if we'll stay above a set SOC%
|
||||||
|
# * set the battery system to charge if we won't
|
||||||
|
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# general imports
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
# >>> To install 'pymodbus' you have to execute the following linux command: pip3 install pymodbus
|
||||||
|
# imports for Modbus
|
||||||
|
from pymodbus.constants import Endian
|
||||||
|
from pymodbus.payload import BinaryPayloadDecoder
|
||||||
|
from pymodbus.client.tcp import ModbusTcpClient as ModbusClient
|
||||||
|
from pymodbus.diag_message import *
|
||||||
|
from pymodbus.file_message import *
|
||||||
|
from pymodbus.other_message import *
|
||||||
|
from pymodbus.mei_message import *
|
||||||
|
#from pymodbus.compat import iteritems
|
||||||
|
|
||||||
|
# imports for using enumerations
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
# enumeration by using a class. value of the enum ( 1-8 ) is irrelevant!
|
||||||
|
# use the method getRegisterLength() instead
|
||||||
|
class DataType(Enum):
|
||||||
|
String8 = 1
|
||||||
|
String16 = 2
|
||||||
|
String32 = 3
|
||||||
|
Int16 = 4
|
||||||
|
UInt16 = 5
|
||||||
|
Int32 = 6
|
||||||
|
UInt32 = 7
|
||||||
|
Float32 = 8
|
||||||
|
UInt64 = 7
|
||||||
|
|
||||||
|
# Returns the length (amount) of the registers.
|
||||||
|
# This corresponds to the value from the Fronius Excel list (column "Size").
|
||||||
|
# This refers to how many registers the Mobus function read_holding_registers() must read to get the complete value
|
||||||
|
def getRegisterLength(self):
|
||||||
|
|
||||||
|
if (self == DataType.String8) or (self == DataType.UInt64):
|
||||||
|
return int(4)
|
||||||
|
elif (self == DataType.String16):
|
||||||
|
return int(8)
|
||||||
|
elif (self == DataType.String32):
|
||||||
|
return int(16)
|
||||||
|
elif (self == DataType.Int16) or (self == DataType.UInt16):
|
||||||
|
return int(1)
|
||||||
|
elif (self == DataType.Int32) or (self == DataType.UInt32) or (self == DataType.Float32):
|
||||||
|
return int(2)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------[ private ]---
|
||||||
|
# | The Main Entry Point |
|
||||||
|
# ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
def main():
|
||||||
|
|
||||||
|
#print ("Current Time: " + datetime.datetime.now().strftime('%H:%M:%S'))
|
||||||
|
|
||||||
|
# Open a new Modbus connection to the fronius inverter (e.g. Symo 10.3)
|
||||||
|
modbusClient = ModbusClient("172.19.107.211", port=502, timeout=10)
|
||||||
|
modbusClient.connect()
|
||||||
|
|
||||||
|
# The modbus addresses of the registers are documented in the following lists:
|
||||||
|
# - Inverter_Register_Map_Float_v1.0_with_SYMOHYBRID_MODEL_124.xlsx
|
||||||
|
# - Meter_Register_Map_Float_v1.0.xlsx
|
||||||
|
# Goto: https://www.fronius.com/en/photovoltaics/downloads and search for "Modbus Sunspec Maps, State Codes und Events"
|
||||||
|
# Downloads the hole ZIP package and enjoy the documentation ;-)
|
||||||
|
#
|
||||||
|
# Gen 24 specific:
|
||||||
|
# Gen24_Primo_Symo_Inverter_Register_Map_Float_storage.xlsx
|
||||||
|
# go to https://www.fronius.com/en/solar-energy/installers-partners/downloads
|
||||||
|
# search for "GEN24 modbus" to find GEN24 related modbus documentation (register addresses are provided in the .zip file)
|
||||||
|
#
|
||||||
|
# Note: In this script you have to specify a data type when calling the method getRegisterValue(). This corresponds
|
||||||
|
# to the value from the Fronius Excel list (column "Type").
|
||||||
|
#
|
||||||
|
# The optional parameter "unitNo" of method getRegisterValue() is used to specify from which device the
|
||||||
|
# data is to be read. The default value is 1 which corresponds to the inverter.
|
||||||
|
# If you want to read data from the SmartMeter, 240 must be used instead.
|
||||||
|
|
||||||
|
# to use this script, adapt and enhance the following list with tuples, containing the follwing information
|
||||||
|
# [REGISTERADDRESS, DATATYPE, MODBUSUNITNUMBER]
|
||||||
|
# the MODBUSUNITNUMBER is commonly 1 for the inverter and a user defined value for the primary smart meter (common values are 200 or 240)
|
||||||
|
|
||||||
|
print ("Register Overview:")
|
||||||
|
|
||||||
|
reg_map = (
|
||||||
|
[40092, DataType.Float32, 1], # Inverter AC Output in W
|
||||||
|
[40285, DataType.UInt16, 1], # PV String 1 Output in W (scaled)
|
||||||
|
[40305, DataType.UInt16, 1], # PV String 2 Output in W (scaled)
|
||||||
|
[40267, DataType.Int16, 1], # DC Scaling Factor
|
||||||
|
[40361, DataType.UInt16, 1], # Battery SoC in % (scaled, SF -2)
|
||||||
|
[40325, DataType.UInt16, 1], # Energy To Battery in W (scaled, charging)
|
||||||
|
[40327, DataType.UInt16, 1], # CHG Percent? as per mqtt-fronius
|
||||||
|
[40345, DataType.UInt16, 1], # Energy From Battery in W (scaled, discharging)
|
||||||
|
[40098, DataType.Float32, 200], # Energy to/from grid (smart meter, positive values: consumption)
|
||||||
|
)
|
||||||
|
|
||||||
|
# for reg in reg_map:
|
||||||
|
# tmp_reg = getRegisterValue(modbusClient, reg[0], reg[1], reg[2])
|
||||||
|
# print ("Register " + str(reg[0]) + ": " + str(tmp_reg))
|
||||||
|
|
||||||
|
|
||||||
|
target_map = {
|
||||||
|
"soc": [40362, DataType.UInt16, 1, "SoC %"],
|
||||||
|
"socSF": [40376, DataType.Int16, 1, "Battery SOC Scaling Factor"],
|
||||||
|
"to_bat": [40325, DataType.UInt16, 1, "cW to Battery"],
|
||||||
|
"from_bat": [40345, DataType.UInt16, 1, "cW from Battery"],
|
||||||
|
"DCSF": [40267, DataType.Int16, 1, "DC scaling factor"],
|
||||||
|
"PWSF": [40268, DataType.Int16, 1, "Power Scaling Factor"]
|
||||||
|
}
|
||||||
|
|
||||||
|
soc = getRegisterValue(modbusClient,
|
||||||
|
target_map["soc"][0],
|
||||||
|
target_map["soc"][1],
|
||||||
|
target_map["soc"][2])
|
||||||
|
|
||||||
|
socSF = getRegisterValue(modbusClient,
|
||||||
|
target_map["socSF"][0],
|
||||||
|
target_map["socSF"][1],
|
||||||
|
target_map["socSF"][2])
|
||||||
|
|
||||||
|
print(" SoC: " + str(soc))
|
||||||
|
print("SoCSF: " + str(socSF))
|
||||||
|
print(" SoC: %.2f %%" % (float(soc * 10 ** socSF)))
|
||||||
|
|
||||||
|
PWSF = getRegisterValue(modbusClient,
|
||||||
|
target_map["PWSF"][0],
|
||||||
|
target_map["PWSF"][1],
|
||||||
|
target_map["PWSF"][2])
|
||||||
|
|
||||||
|
print(" PWSF: " + str(PWSF))
|
||||||
|
|
||||||
|
to_bat = getRegisterValue(modbusClient,
|
||||||
|
target_map["to_bat"][0],
|
||||||
|
target_map["to_bat"][1],
|
||||||
|
target_map["to_bat"][2])
|
||||||
|
|
||||||
|
print("To Bat: " + str(to_bat))
|
||||||
|
|
||||||
|
print("To Bat: %.2f" % (to_bat * 10 ** (int(PWSF))))
|
||||||
|
|
||||||
|
from_bat = getRegisterValue(modbusClient,
|
||||||
|
target_map["from_bat"][0],
|
||||||
|
target_map["from_bat"][1],
|
||||||
|
target_map["from_bat"][2])
|
||||||
|
|
||||||
|
print("Fr Bat: " + str(from_bat * 10 ** (int(PWSF))))
|
||||||
|
|
||||||
|
|
||||||
|
num_avgs = 5
|
||||||
|
count = 1
|
||||||
|
while count < num_avgs:
|
||||||
|
print("To AVG: " + str(get_avg_scaled(modbusClient, target_map["to_bat"], target_map["PWSF"])))
|
||||||
|
print("Fr AVG: " + str(get_avg_scaled(modbusClient, target_map["from_bat"], target_map["PWSF"])))
|
||||||
|
count += 1
|
||||||
|
modbusClient.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---
|
||||||
|
# ---[ private ]---
|
||||||
|
def get_avg_scaled(modbusClient, reg, sfreg, points=50):
|
||||||
|
local_data = []
|
||||||
|
count = 0
|
||||||
|
while ( count < points):
|
||||||
|
local_data.append(
|
||||||
|
getRegisterValue(
|
||||||
|
modbusClient, reg[0], reg[1], reg[2]) * 10 ** getRegisterValue(
|
||||||
|
modbusClient, sfreg[0], sfreg[1], sfreg[2]))
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return sum(local_data) / len(local_data)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------[ private ]---
|
||||||
|
# | Gets a value from the inverter |
|
||||||
|
# | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
|
# | Input parameters: |
|
||||||
|
# | -> device ModbusClient An open connection to the modbus device (inverter or smartmeter) |
|
||||||
|
# | -> address INT The starting address to read from |
|
||||||
|
# | -> dataType DataType The DataType of registers to read |
|
||||||
|
# | -> humidity INT The slave unit this request is targeting |
|
||||||
|
# | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
|
# | Return value: |
|
||||||
|
# | <- result STRING Value of the defined address |
|
||||||
|
# ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
def getRegisterValue(device, address, dataType, unitNo=1):
|
||||||
|
|
||||||
|
#print (" Adr: " + str(address) + " Name: " + dataType.name)
|
||||||
|
|
||||||
|
# Now we can read the data of the register with a Modbus function
|
||||||
|
# In the fronius documentation it is described that you have to subtract 1 from the actual address.
|
||||||
|
result = device.read_holding_registers(address-1, dataType.getRegisterLength(), slave=unitNo)
|
||||||
|
|
||||||
|
if (result.isError()) :
|
||||||
|
return "n.a."
|
||||||
|
|
||||||
|
#print (" value: " + str(result.registers))
|
||||||
|
|
||||||
|
# The values from Modbus must now be reformatted accordingly
|
||||||
|
# How to do this reformatting depends on the DataType
|
||||||
|
decoder = BinaryPayloadDecoder.fromRegisters(result.registers, byteorder=Endian.Big, wordorder=Endian.Big)
|
||||||
|
|
||||||
|
if (dataType == DataType.String8) or (dataType == DataType.String16) or (dataType == DataType.String32):
|
||||||
|
return str(decoder.decode_string(16).decode('utf-8'))
|
||||||
|
|
||||||
|
elif (dataType == DataType.Int16):
|
||||||
|
return decoder.decode_16bit_int()
|
||||||
|
|
||||||
|
elif (dataType == DataType.UInt16):
|
||||||
|
return decoder.decode_16bit_uint()
|
||||||
|
|
||||||
|
elif (dataType == DataType.Int32):
|
||||||
|
return decoder.decode_32bit_int()
|
||||||
|
|
||||||
|
elif (dataType == DataType.UInt32):
|
||||||
|
return decoder.decode_32bit_uint()
|
||||||
|
|
||||||
|
elif (dataType == DataType.Float32):
|
||||||
|
return decoder.decode_32bit_float()
|
||||||
|
|
||||||
|
else:
|
||||||
|
return str(decoder.decode_bits())
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------[ private ]---
|
||||||
|
# | Formats the given nuber (powerValue) into a well-formed and readable text |
|
||||||
|
# | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
|
# | Input parameters: |
|
||||||
|
# | -> powerValue FLOAT The value to format |
|
||||||
|
# | ----------------------------------------------------------------------------------------------------------------- |
|
||||||
|
# | Return value: |
|
||||||
|
# | <- formatedText STRING A well-formed and readable text containing the powerValue |
|
||||||
|
# ---------------------------------------------------------------------------------------------------------------------
|
||||||
|
def formatPowerText(powerValue):
|
||||||
|
|
||||||
|
formatedText = ""
|
||||||
|
|
||||||
|
# Over 1000 'kilo Watt' will be displayed instead of 'Watt'
|
||||||
|
if abs(powerValue) > 1000:
|
||||||
|
formatedText = "{0} kW".format(str('{:0.2f}'.format(powerValue / 1000))).replace('.', ',')
|
||||||
|
else:
|
||||||
|
formatedText = "{0} W".format(str('{:.0f}'.format(powerValue))).replace('.', ',')
|
||||||
|
|
||||||
|
return formatedText
|
||||||
|
|
||||||
|
|
||||||
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
# | Call the main function to start this script |
|
||||||
|
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
main()
|
||||||
|
|
Loading…
Reference in a new issue