diff --git a/.gitignore b/.gitignore index 6f7f2e5..9d984e9 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,8 @@ venv.bak/ # mypy .mypy_cache/ +*.bak +out*.txt +nohup.out +/bak + diff --git a/EM24-proxy-tcp.cmd b/EM24-proxy-tcp.cmd new file mode 100644 index 0000000..09918e0 --- /dev/null +++ b/EM24-proxy-tcp.cmd @@ -0,0 +1,2 @@ +echo use -v for more information +python EM24DINAV23XE1X-proxy-tcp.py -c SE-MTR-3Y-400V-A.conf %* diff --git a/EM24-proxy-tcp.sh b/EM24-proxy-tcp.sh new file mode 100644 index 0000000..8da5ea7 --- /dev/null +++ b/EM24-proxy-tcp.sh @@ -0,0 +1,4 @@ +#!/bin/bash +echo use -v for more information +python EM24DINAV23XE1X-proxy-tcp.py -c SE-MTR-3Y-400V-A.conf $* + diff --git a/EM24DINAV23XE1X-proxy-tcp.py b/EM24DINAV23XE1X-proxy-tcp.py new file mode 100644 index 0000000..3b58e8b --- /dev/null +++ b/EM24DINAV23XE1X-proxy-tcp.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 + +import argparse +import configparser +import importlib +import logging +from operator import truediv +import sys +import threading +import time + +from pymodbus.server.sync import StartTcpServer +from pymodbus.server.sync import ModbusTcpServer +from pymodbus.constants import Endian +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.transaction import ModbusRtuFramer +from pymodbus.datastore import ModbusSlaveContext +from pymodbus.datastore import ModbusServerContext +from pymodbus.payload import BinaryPayloadBuilder + + +class EM24SlaveContext(ModbusSlaveContext): + def getValues(self, fx, address, count=1): + if (address == 11 and count==1): + logger.info("Gavazzi Model number 1648 supplied") + return [1648] + return super().getValues(fx, address, count) + + + +class ModbusMyTcpServer(ModbusTcpServer): + clientCounter = 0 + + def process_request(self, request, client): + """ Callback for connecting a new client thread + + :param request: The request to handle + :param client: The address of the client + """ + self.clientCounter += 1 + logger = logging.getLogger() + logger.info("Started thread to serve client at " + str(client) + " clientCounter = " + str(self.clientCounter)) + super().process_request(request,client) + + def shutdown(self): + """ Stops the serve_forever loop. + + Overridden to signal handlers to stop. + """ + logger = logging.getLogger() + logger.info("shutdown to serve client") + super().shutdown() + + def server_close(self): + """ Callback for stopping the running server + """ + logger = logging.getLogger() + logger.debug("Modbus server stopped") + super().server_close() + + +# --------------------------------------------------------------------------- # +# Creation Factorie +# --------------------------------------------------------------------------- # +def StartMyTcpServer(context=None, identity=None, address=None, + custom_functions=[], **kwargs): + """ A factory to start and run a tcp modbus server + + :param context: The ModbusServerContext datastore + :param identity: An optional identify structure + :param address: An optional (interface, port) to bind to. + :param custom_functions: An optional list of custom function classes + supported by server instance. + :param ignore_missing_slaves: True to not send errors on a request to a + missing slave + """ + framer = kwargs.pop("framer", ModbusSocketFramer) + server = ModbusMyTcpServer(context, framer, identity, address, **kwargs) + + for f in custom_functions: + server.decoder.register(f) + server.serve_forever() + + + +def t_update(ctx, stop, module, device, refresh): + + this_t = threading.currentThread() + logger = logging.getLogger() + + while not stop.is_set(): + try: + values = module.values(device) + if not values: + logger.debug(f"{this_t.name}: no new values") + continue + + meterValues = values["connected_meters"]["Meter1"] + + if logger.isEnabledFor(logging.DEBUG): + logger.info("current:") + logger.info(values.get('l1_current')) + logger.info(values.get('l2_current')) + logger.info(values.get('l3_current')) + + logger.info("voltage_ln:"+str(values.get('voltage_ln'))) + logger.info(values.get('l1n_voltage')) + logger.info(values.get('l2n_voltage')) + logger.info(values.get('l3n_voltage')) + + logger.info("voltage_ll:"+str(values.get('voltage_ll'))) + logger.info(values.get('l12_voltage')) + logger.info(values.get('l23_voltage')) + logger.info(values.get('l31_voltage')) + + + logger.info("frequency:"+str(values.get('frequency'))) + + logger.info("power:"+str(values.get('power_active'))) + logger.info(values.get('l1_power_active')) + logger.info(values.get('l2_power_active')) + logger.info(values.get('l3_power_active')) + + logger.info("power_apparent:"+str(values.get('power_apparent'))) + logger.info(values.get('l1_power_apparent')) + logger.info(values.get('l2_power_apparent')) + logger.info(values.get('l3_power_apparent')) + + logger.info("power_reactive:"+str(values.get('power_reactive'))) + logger.info(values.get('l1_power_reactive')) + logger.info(values.get('l2_power_reactive')) + logger.info(values.get('l3_power_reactive')) + + logger.info("power_factor:"+str(values.get('power_factor'))) + logger.info(values.get('l1_power_factor')) + logger.info(values.get('l2_power_factor')) + logger.info(values.get('l3_power_factor')) + + logger.info("export_energy_active:"+str(values.get('export_energy_active')/1000)) + logger.info(values.get('l1_export_energy_active')/1000) + logger.info(values.get('l2_export_energy_active')/1000) + logger.info(values.get('l3_export_energy_active')/1000) + + logger.info("import_energy_active:"+str(values.get('import_energy_active')/1000)) + logger.info(values.get('l1_import_energy_active')/1000) + logger.info(values.get('l2_import_energy_active')/1000) + logger.info(values.get('l3_import_energy_active')/1000) + + logger.info("energy_apparent:"+str(values.get('energy_apparent')/1000)) + logger.info(values.get('l1_energy_apparent')/1000) + logger.info(values.get('l2_energy_apparent')/1000) + logger.info(values.get('l3_energy_apparent')/1000) + + + + block_0 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_0.add_32bit_int(int(values.get('l1n_voltage')*10)) # l1-n voltage * 10 + block_0.add_32bit_int(int(values.get('l2n_voltage')*10)) # l2-n voltage + block_0.add_32bit_int(int(values.get('l3n_voltage')*10)) # l3-n voltage + block_0.add_32bit_int(int(values.get('l12_voltage')*10)) # l1-l2 voltage + block_0.add_32bit_int(int(values.get('l23_voltage')*10)) # l2-l3 voltage + block_0.add_32bit_int(int(values.get('l31_voltage')*10)) # l3-l1 voltage + block_0.add_32bit_int(int(values.get('l1_current')*1000)) # current l1 * 1000 + block_0.add_32bit_int(int(values.get('l2_current')*1000)) # current l2 + block_0.add_32bit_int(int(values.get('l3_current')*1000)) # current l3 + block_0.add_32bit_int(int(values.get('l1_power_active')*-10)) # power l1 *10 + block_0.add_32bit_int(int(values.get('l2_power_active')*-10)) # power l2 + block_0.add_32bit_int(int(values.get('l3_power_active')*-10)) # power l3 + block_0.add_32bit_int(int(values.get('l1_power_apparent')*-10)) # apparent power l1 *10 + block_0.add_32bit_int(int(values.get('l2_power_apparent')*-10)) # apparent power l2 + block_0.add_32bit_int(int(values.get('l3_power_apparent')*-10)) # apparent power l3 + block_0.add_32bit_int(int(values.get('l1_power_reactive')*-10)) # reactive power l1 *10 + block_0.add_32bit_int(int(values.get('l2_power_reactive')*-10)) # reactive power l2 + block_0.add_32bit_int(int(values.get('l3_power_reactive')*-10)) # reactive power l3 + block_0.add_32bit_int(int(values.get('voltage_ln')*10)) # l-n voltage *10 + block_0.add_32bit_int(int(values.get('voltage_ll')*10)) # l-l voltage + block_0.add_32bit_int(int(values.get('power_active')*-10)) # total power *10 + block_0.add_32bit_int(int(values.get('power_apparent')*-10)) # total apparent power + block_0.add_32bit_int(int(values.get('power_reactive')*-10)) # total reactive power + block_0.add_16bit_int(int(values.get('l1_power_factor')*10)) # power factor l1 *1000 + block_0.add_16bit_int(int(values.get('l2_power_factor')*10)) # power factor l2 + block_0.add_16bit_int(int(values.get('l3_power_factor')*10)) # power factor l3 + block_0.add_16bit_int(int(values.get('power_factor')*10)) # power factor + block_0.add_16bit_int(0) # Value –1 correspond to L1-L3-L2 sequence, value 0 correspond to L1-L2-L3 sequence (this value is meaningful only in case of 3-phase systems) + + block_0.add_16bit_uint(int(values.get('frequency')*10)) # line frequency *10 + + block_0.add_32bit_int(int(values.get('import_energy_active')/100)) # imported active energy *10 + block_0.add_32bit_int(int(values.get('energy_apparent')/100)) # imported active energy + block_0.add_32bit_int(56) # demand power + block_0.add_32bit_int(58) # maximum demand power + block_0.add_32bit_int(int(values.get('import_energy_active')/100)) # imported active energy + block_0.add_32bit_int(int(values.get('energy_apparent')/100)) # imported active energy + block_0.add_32bit_int(int(values.get('l1_import_energy_active')/100)) # imported active energy l1 + block_0.add_32bit_int(int(values.get('l2_import_energy_active')/100)) # imported active energy l2 + block_0.add_32bit_int(int(values.get('l3_import_energy_active')/100)) # imported active energy l3 + block_0.add_32bit_int(10) # total active energy Tarif 1 + block_0.add_32bit_int(20) # total active energy Tarif 2 + block_0.add_32bit_int(30) # total active energy Tarif 3 + block_0.add_32bit_int(40) # total active energy Tarif 4 + block_0.add_32bit_int(int(values.get('export_energy_active')/100)) # total exported active energy non-reset ) + block_0.add_32bit_int(int(values.get('import_energy_active')/100)) # import active energy non-reset + block_0.add_32bit_int(2400) # hour *100 + block_0.add_32bit_int(11) # total apparent energy Tarif 1 *10 + block_0.add_32bit_int(22) # total apparent energy Tarif 2 + block_0.add_32bit_int(33) # total apparent energy Tarif 3 + block_0.add_32bit_int(44) # total apparent energy Tarif 4 + block_0.add_32bit_int(118) # apparent demand power + block_0.add_32bit_int(120) # apparent demand power max + block_0.add_32bit_int(122) # DMD A max *10 + ctx.setValues(3, 0, block_0.to_registers()) + ctx.setValues(4, 0, block_0.to_registers()) + + block_254 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_254.add_32bit_int(2400) # hour *100 *100 + block_254.add_32bit_int(256) # unused *100 + block_254.add_32bit_int(int(values.get('voltage_ln')*10)) # l-n voltage *10 + block_254.add_32bit_int(int(values.get('voltage_ll')*10)) # l-l voltage + block_254.add_32bit_int(int(values.get('power_active')*-10)) # total power *10 + block_254.add_32bit_int(int(values.get('power_apparent')*-10)) # total apparent power + block_254.add_32bit_int(int(values.get('power_reactive')*-10)) # total reactive power + block_254.add_32bit_int(int(values.get('power_factor')*10)) # power factor + block_254.add_32bit_int(0) # Value –1 correspond to L1-L3-L2 sequence, value 0 correspond to L1-L2-L3 sequence (this value is meaningful only in case of 3-phase systems) + block_254.add_32bit_int(int(values.get('frequency')*10)) # line frequency *10 + block_254.add_32bit_int(int(values.get('import_energy_active')/100)) # imported active energy + block_254.add_32bit_int(int(values.get('energy_apparent')/100)) # imported active energy + block_254.add_32bit_int(int(values.get('export_energy_active')/100)) # total exported active energy non-reset ) + block_254.add_32bit_int(int(values.get('import_energy_active')/100)) # imported active energy non-reset + block_254.add_32bit_int(56) # demand power + block_254.add_32bit_int(58) # maximum demand power + + + + + block_254.add_32bit_int(int(values.get('l12_voltage')*10)) # l1-l2 voltage + block_254.add_32bit_int(int(values.get('l1n_voltage')*10)) # l1-n voltage * 10 + block_254.add_32bit_int(int(values.get('l1_current')*100)) # current l1 * 1000 + block_254.add_32bit_int(int(values.get('l1_power_active')*-10)) # power l1 *10 + block_254.add_32bit_int(int(values.get('l1_power_apparent')*-10)) # apparent power l1 *10 + block_254.add_32bit_int(int(values.get('l1_power_reactive')*-10)) # reactive power l1 *10 + block_254.add_32bit_int(int(values.get('l1_power_factor')*1000)) # power factor l1 *1000 + + block_254.add_32bit_int(int(values.get('l23_voltage')*10)) # l2-l3 voltage + block_254.add_32bit_int(int(values.get('l2n_voltage')*10)) # l2-n voltage + block_254.add_32bit_int(int(values.get('l2_current')*100)) # current l2 + block_254.add_32bit_int(int(values.get('l2_power_active')*-10)) # power l2 + block_254.add_32bit_int(int(values.get('l2_power_apparent')*-10)) # apparent power l2 + block_254.add_32bit_int(int(values.get('l2_power_reactive')*-10)) # reactive power l2 + block_254.add_32bit_int(int(values.get('l2_power_factor')*1000)) # power factor l2 + + block_254.add_32bit_int(int(values.get('l31_voltage')*10)) # l3-l1 voltage + block_254.add_32bit_int(int(values.get('l3n_voltage')*10)) # l3-n voltage + block_254.add_32bit_int(int(values.get('l3_current')*100)) # current l3 + block_254.add_32bit_int(int(values.get('l3_power_active')*-10)) # power l3 + block_254.add_32bit_int(int(values.get('l3_power_apparent')*-10)) # apparent power l3 + block_254.add_32bit_int(int(values.get('l3_power_reactive')*-10)) # reactive power l3 + block_254.add_32bit_int(int(values.get('l3_power_factor')*1000)) # power factor l3 + + block_254.add_32bit_int(0) # Value –1 correspond to L1-L3-L2 sequence, value 0 correspond to L1-L2-L3 sequence (this value is meaningful only in case of 3-phase systems) + + block_254.add_32bit_int(int(values.get('import_energy_active')/100)) # imported active energy + block_254.add_32bit_int(int(values.get('energy_apparent')/100)) # imported active energy + block_254.add_32bit_int(int(values.get('l1_import_energy_active')/100)) # imported active energy l1 + block_254.add_32bit_int(int(values.get('l2_import_energy_active')/100)) # imported active energy l2 + block_254.add_32bit_int(int(values.get('l3_import_energy_active')/100)) # imported active energy l3 + block_254.add_32bit_int(10) # total active energy Tarif 1 + block_254.add_32bit_int(20) # total active energy Tarif 2 + block_254.add_32bit_int(30) # total active energy Tarif 3 + block_254.add_32bit_int(40) # total active energy Tarif 4 + block_254.add_32bit_int(346) # unused *100 + block_254.add_32bit_int(348) # unused *100 + block_254.add_32bit_int(350) # unused *100 + block_254.add_32bit_int(352) # unused *100 + block_254.add_32bit_int(11) # total apparent energy Tarif 1 *10 + block_254.add_32bit_int(22) # total apparent energy Tarif 2 + block_254.add_32bit_int(33) # total apparent energy Tarif 3 + block_254.add_32bit_int(44) # total apparent energy Tarif 4 + block_254.add_32bit_int(262) # unused *100 + block_254.add_32bit_int(264) # unused *100 + block_254.add_32bit_int(266) # unused *100 + block_254.add_32bit_int(268) # unused *100 + block_254.add_32bit_int(270) # unused *100 + block_254.add_32bit_int(272) # unused *100 + block_254.add_32bit_int(274) # unused *100 + block_254.add_32bit_int(276) # unused *100 + block_254.add_32bit_int(118) # apparent demand power + block_254.add_32bit_int(120) # apparent demand power max + block_254.add_32bit_int(122) # DMD A max *10 + ctx.setValues(3, 254, block_254.to_registers()) + ctx.setValues(4, 254, block_254.to_registers()) + + ## unused values + # "energy_reactive" # total reactive energy + # "l1_export_energy_active", 0)) # exported energy l1 + # "l2_export_energy_active", 0)) # exported energy l2 + # "l3_export_energy_active", 0)) # exported energy l3 + # "l1_energy_reactive", 0)) # reactive energy l1 + # "l2_energy_reactive", 0)) # reactive energy l2 + # "l3_energy_reactive", 0)) # reactive energy l3 + # "l1_energy_apparent", 0)) # apparent energy l1 + # "l2_energy_apparent", 0)) # apparent energy l2 + # "l3_energy_apparent", 0)) # apparent energy l3 + # "minimum_demand_power_active", 0)) # minimum demand power + # "l1_demand_power_active", 0)) # demand power l1 + # "l2_demand_power_active", 0)) # demand power l2 + # "l3_demand_power_active", 0)) # demand power l3 + except Exception as e: + logger.critical(f"{this_t.name}: {e}") + finally: + time.sleep(refresh) + + +if __name__ == "__main__": + argparser = argparse.ArgumentParser() + argparser.add_argument("-c", "--config", type=str, default="semp-tcp.conf") + argparser.add_argument("-v", "--verbose", action="store_true", default=False) + args = argparser.parse_args() + + default_config = { + "server": { + "address": "0.0.0.0", + "port": 502, + "framer": "socket", + "log_level": "INFO", + "meters": 'Meter1' + }, + "meters": { + "dst_address": 2, + "type": "generic", + "ct_current": 5, + "ct_inverted": 0, + "phase_offset": 120, + "serial_number": 987654, + "refresh_rate": 5 + } + } + + confparser = configparser.ConfigParser() + confparser.read(args.config) + + if not confparser.has_section("server"): + confparser["server"] = default_config["server"] + + log_handler = logging.StreamHandler(sys.stdout) + log_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")) + + logger = logging.getLogger() + logger.setLevel(getattr(logging, confparser["server"].get("log_level", fallback=default_config["server"]["log_level"]).upper())) + logger.addHandler(log_handler) + + if args.verbose: + logger.setLevel(logging.DEBUG) + + slaves = {} + threads = [] + thread_stops = [] + + try: + if confparser.has_option("server", "meters"): + meters = [m.strip() for m in confparser["server"].get("meters", fallback=default_config["server"]["meters"]).split(',')] + + for meter in meters: + address = confparser[meter].getint("dst_address", fallback=default_config["meters"]["dst_address"]) + meter_type = confparser[meter].get("type", fallback=default_config["meters"]["type"]) + meter_module = importlib.import_module(f"devices.{meter_type}") + meter_device = meter_module.device(confparser[meter]) + + EM24_slave_ctx = EM24SlaveContext() + # SE7K_slave_ctx = ModbusSlaveContext() + + # block_11 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + # block_11.add_16bit_int(1648) + # EM24_slave_ctx.setValues(3, 11, block_11.to_registers()) + # EM24_slave_ctx.setValues(4, 11, block_11.to_registers()) + + block_0 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_0.add_32bit_int(1234) + EM24_slave_ctx.setValues(3, 0, block_0.to_registers()) + EM24_slave_ctx.setValues(4, 0, block_0.to_registers()) + + block_770 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_770.add_16bit_int(4126) # Version and revision measurment module + block_770.add_16bit_int(68) # + block_770.add_16bit_int(4127) # Version and revision communication module + block_770.add_16bit_int(67) # + block_770.add_16bit_int(0) # Current tariff + EM24_slave_ctx.setValues(3, 770, block_770.to_registers()) + EM24_slave_ctx.setValues(4, 770, block_770.to_registers()) + + block_848 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_848.add_16bit_int(4128) # Measurement module’s firmware CRC + EM24_slave_ctx.setValues(3, 848, block_848.to_registers()) + EM24_slave_ctx.setValues(4, 848, block_848.to_registers()) + + block_20480 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_20480.add_string("MB24DINAV23XE1X") + EM24_slave_ctx.setValues(3, 20480, block_20480.to_registers()) + EM24_slave_ctx.setValues(4, 20480, block_20480.to_registers()) + + block_41216 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_41216.add_16bit_int(3) # Front selector status + EM24_slave_ctx.setValues(3, 41216, block_41216.to_registers()) + EM24_slave_ctx.setValues(4, 41216, block_41216.to_registers()) + + block_4096 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_4096.add_16bit_int(9999) # PASSWORD + block_4096.add_16bit_int(0) # unused + block_4096.add_16bit_int(0) # Measuring system + block_4096.add_32bit_int(10) # Current transformer ratio + block_4096.add_32bit_int(10) # Voltage transformer ratio + block_4096.add_16bit_int(1) # unused + block_4096.add_16bit_int(2) # unused + block_4096.add_16bit_int(3) # unused + block_4096.add_16bit_int(4) # unused + block_4096.add_16bit_int(5) # unused + block_4096.add_16bit_int(6) # unused + block_4096.add_16bit_int(7) # unused + block_4096.add_16bit_int(8) # unused + block_4096.add_16bit_int(9) # unused + block_4096.add_32bit_int(15) # Interval time + EM24_slave_ctx.setValues(3, 4096, block_4096.to_registers()) + EM24_slave_ctx.setValues(4, 4096, block_4096.to_registers()) + + block_4360 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_4360.add_16bit_int(2) # PASSWORD + block_4360.add_16bit_int(2) # PASSWORD + EM24_slave_ctx.setValues(3, 4360, block_4360.to_registers()) + EM24_slave_ctx.setValues(4, 4360, block_4360.to_registers()) + + block_40960 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_40960.add_16bit_int(1) # Type of application + block_40960.add_16bit_int(3) # Default page for selector position “LOCK” + block_40960.add_16bit_int(1) # Default page for selector position “1” + block_40960.add_16bit_int(3) # Default page for selector position “2” + block_40960.add_16bit_int(3) # Default page for selector position “kvarh” + block_40960.add_16bit_int(1) # ID code of user 1 + block_40960.add_16bit_int(2) # ID code of user 2 + block_40960.add_16bit_int(3) # ID code of user 3 + EM24_slave_ctx.setValues(3, 40960, block_40960.to_registers()) + EM24_slave_ctx.setValues(4, 40960, block_40960.to_registers()) + + update_t_stop = threading.Event() + update_t = threading.Thread( + target=t_update, + name=f"t_update_{address}", + args=( + EM24_slave_ctx, + update_t_stop, + meter_module, + meter_device, + confparser[meter].getfloat("refresh_rate", fallback=default_config["meters"]["refresh_rate"]) + ) + ) + + threads.append(update_t) + thread_stops.append(update_t_stop) + + slaves.update({address: EM24_slave_ctx}) + slaves.update({2: EM24_slave_ctx}) + logger.info(f"Created {update_t}: {meter} {meter_type} {meter_device}") + + if not slaves: + logger.warning(f"No meters defined in {args.config}") + + config_framer = confparser["server"].get("framer", fallback=default_config["server"]["framer"]) + framer = False + + if config_framer == "socket": + framer = ModbusSocketFramer + elif config_framer == "rtu": + framer = ModbusRtuFramer + + identity = ModbusDeviceIdentification() + server_ctx = ModbusServerContext(slaves=slaves, single=False) + + time.sleep(1) + + for t in threads: + t.start() + logger.info(f"Starting {t}") + + StartMyTcpServer( + server_ctx, + framer=framer, + identity=identity, + address=( + confparser["server"].get("address", fallback=default_config["server"]["address"]), + confparser["server"].getint("port", fallback=default_config["server"]["port"]) + ) + ) + except KeyboardInterrupt: + pass + finally: + for t_stop in thread_stops: + t_stop.set() + for t in threads: + t.join() diff --git a/SE-MTR-3Y-400V-A.conf b/SE-MTR-3Y-400V-A.conf new file mode 100644 index 0000000..2de4e5f --- /dev/null +++ b/SE-MTR-3Y-400V-A.conf @@ -0,0 +1,67 @@ +[server] +# Serving IP address. +# optional, default: all interfaces +address=0.0.0.0 +# ip-address=0.0.0.0 +# ip_address=0.0.0.0 + +# Serving port. +# optional, default: 5502 +port = 502 + +# Modbus frame type, set to rtu for Modbus RTU over TCP +# optional, default: socket +#framer = socket + +# Logging level, CRITICAL, ERROR, WARNING, INFO, DEBUG +# optional, default: INFO +log_level = INFO + +# Masqueraded meters, comma separated. +# optional, default: '' +meters = solaredge-inverter + +[solaredge-inverter] +# the solarage inverter should have an SE-MTR-3Y-400V-A meter attached +type=solaredge-inverter +host=solaredge.fritz.box +#host=raspberrypi.fritz.box +port=502 +src_address=2 +dst_address=1 + + +# Meters defined in [server] need a config section, one per meter. +# Depending on the type of meter that is to be masqueraded, you can +# define a number of generic and type specific variables. + +# Modbus address of the meter as defined in the SolarEdge inverter. +# This value needs to be unique. +# optional, default: 2 +#dst_address = 2 + +# Source meter type, which corresponds to a script in /devices. +# The generic.py device returns null values. +# optional, default: generic +#type = generic + +# Masqueraded serial number. +# Need not be correct, must be unique, must be an integer. +# optional, default: 987654 +#serial_number = 987654 + +# Current transformer amperage rating. +# optional, default: 5 +#ct_current = 50 + +# Current transformer direction inversion, set to 1 if required. +# optional, default: 0 +#ct_inverted = 0 + +# Offset between phases, set to 0, 90, 120 or 180. +# optional, default: 0 +#phase_offset = 120 + +# Number of seconds between value refreshes. +# optional, default: 5 +#refresh_rate = 5 diff --git a/SE7K-EM24-proxy-tcp.cmd b/SE7K-EM24-proxy-tcp.cmd new file mode 100644 index 0000000..2d10031 --- /dev/null +++ b/SE7K-EM24-proxy-tcp.cmd @@ -0,0 +1,2 @@ +echo use -v for more information +python SE7K-EM24-proxy-tcp.py -c SE-MTR-3Y-400V-A.conf %* diff --git a/SE7K-EM24-proxy-tcp.d.sh b/SE7K-EM24-proxy-tcp.d.sh new file mode 100644 index 0000000..14757c5 --- /dev/null +++ b/SE7K-EM24-proxy-tcp.d.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set +x +meinpfad=$(dirname $0) +file=/var/log/SE7K-EM24-proxy.log +logfile=$file +[ -e $file ] && [ $(stat --printf '%s' "$file") -gt 104857600 ] && rm "$file" +file=/var/log/SE7K-EM24-proxy.err +errfile=$file +[ -e $file ] && [ $(stat --printf '%s' "$file") -gt 104857600 ] && rm "$file" +nohup python3 $meinpfad/SE7K-EM24-proxy-tcp.py -c $meinpfad/SE-MTR-3Y-400V-A.conf 2>>$errfile >>$logfile & diff --git a/SE7K-EM24-proxy-tcp.py b/SE7K-EM24-proxy-tcp.py new file mode 100644 index 0000000..8d5d173 --- /dev/null +++ b/SE7K-EM24-proxy-tcp.py @@ -0,0 +1,749 @@ +#!/usr/bin/env python3 + +import argparse +import configparser +import importlib +import logging +import sys +import threading +import time + +from pymodbus.server.sync import StartTcpServer +from pymodbus.server.sync import ModbusTcpServer +from pymodbus.constants import Endian +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.transaction import ModbusRtuFramer +from pymodbus.datastore import ModbusSlaveContext +from pymodbus.datastore import ModbusServerContext +from pymodbus.payload import BinaryPayloadBuilder + + +class EM24SlaveContext(ModbusSlaveContext): + def getValues(self, fx, address, count=1): + if (address == 11 and count==1): + logger.info("Gavazzi Model number 1648 supplied") + return [1648] + return super().getValues(fx, address, count) + + + +class ModbusMyTcpServer(ModbusTcpServer): + clientCounter={} + + def process_request(self, request, client): + """ Callback for connecting a new client thread + + :param request: The request to handle + :param client: The address of the client + """ + self.clientCounter[client[0]] = self.clientCounter.get(client[0],0) + 1 + + logger = logging.getLogger() + if self.clientCounter[client[0]]%1000 == 1: + logger.info("Started thread to serve client at " + str(client[0]) + " clientCounter = " + str(self.clientCounter[client[0]]) + " request"+ str(request)) + + super().process_request(request,client) + + def shutdown(self): + """ Stops the serve_forever loop. + + Overridden to signal handlers to stop. + """ + logger = logging.getLogger() + logger.info("shutdown to serve client") + super().shutdown() + + def server_close(self): + """ Callback for stopping the running server + """ + logger = logging.getLogger() + logger.debug("Modbus server stopped") + super().server_close() + + +# --------------------------------------------------------------------------- # +# Creation Factorie +# --------------------------------------------------------------------------- # +def StartMyTcpServer(context=None, identity=None, address=None, + custom_functions=[], **kwargs): + """ A factory to start and run a tcp modbus server + + :param context: The ModbusServerContext datastore + :param identity: An optional identify structure + :param address: An optional (interface, port) to bind to. + :param custom_functions: An optional list of custom function classes + supported by server instance. + :param ignore_missing_slaves: True to not send errors on a request to a + missing slave + """ + framer = kwargs.pop("framer", ModbusSocketFramer) + server = ModbusMyTcpServer(context, framer, identity, address, **kwargs) + + for f in custom_functions: + server.decoder.register(f) + server.serve_forever() + + +def setMeterValues(values, block): + if not values: + block.add_16bit_uint(0) + block.add_16bit_uint(0) + return + + block.add_16bit_uint(1) + block.add_16bit_uint(65) + block.add_string (values.get("c_manufacturer_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block.add_string (values.get("c_model_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block.add_string (values.get("c_option_str" ,"1234567890123456").ljust(16,' ')) + block.add_string (values.get("c_version_str" ,"1234567890123456").ljust(16,' ')) + block.add_string (values.get("c_serialnumber_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block.add_16bit_int (values.get("c_deviceaddress_int" , 0)) + + block.add_16bit_int (values.get("c_sunspec_did_int" , 103)) + block.add_16bit_int (values.get("c_sunspec_length_int", 50)) + block.add_16bit_uint(values.get("current_int" , 0)) + block.add_16bit_uint(values.get("l1_current_int" , 0)) + block.add_16bit_uint(values.get("l2_current_int" , 0)) + block.add_16bit_uint(values.get("l3_current_int" , 0)) + block.add_16bit_int (values.get("current_scale_int" , 0)) + + block.add_16bit_uint(values.get("voltage_ln_int" , 0)) + block.add_16bit_uint(values.get("l1n_voltage_int" , 0)) + block.add_16bit_uint(values.get("l2n_voltage_int" , 0)) + block.add_16bit_uint(values.get("l3n_voltage_int" , 0)) + block.add_16bit_uint(values.get("voltage_ll_int" , 0)) + block.add_16bit_uint(values.get("l1n_voltage_int" , 0)) + block.add_16bit_uint(values.get("l2n_voltage_int" , 0)) + block.add_16bit_uint(values.get("l3n_voltage_int" , 0)) + block.add_16bit_int (values.get("voltage_scale_int" , 0)) + + block.add_16bit_uint(values.get("frequency_int" , 0)) + block.add_16bit_int (values.get("frequency_scale_int" , 0)) + + block.add_16bit_int(values.get("power_int" , 0)) + block.add_16bit_int(values.get("l1_power_int" , 0)) + block.add_16bit_int(values.get("l2_power_int" , 0)) + block.add_16bit_int(values.get("l3_power_int" , 0)) + block.add_16bit_int (values.get("power_scale_int" , 0)) + + block.add_16bit_int(values.get("power_apparent_int" , 0)) + block.add_16bit_int(values.get("l1_power_apparent_int" , 0)) + block.add_16bit_int(values.get("l2_power_apparent_int" , 0)) + block.add_16bit_int(values.get("l3_power_apparent_int" , 0)) + block.add_16bit_int (values.get("power_apparent_scale_int" , 0)) + + block.add_16bit_int(values.get("power_reactive_int" , 0)) + block.add_16bit_int(values.get("l1_power_reactive_int" , 0)) + block.add_16bit_int(values.get("l2_power_reactive_int" , 0)) + block.add_16bit_int(values.get("l3_power_reactive_int" , 0)) + block.add_16bit_int (values.get("power_reactive_scale_int" , 0)) + + block.add_16bit_int(values.get("power_factor_int" , 0)) + block.add_16bit_int(values.get("l1_power_factor_int" , 0)) + block.add_16bit_int(values.get("l2_power_factor_int" , 0)) + block.add_16bit_int(values.get("l3_power_factor_int" , 0)) + block.add_16bit_int (values.get("power_factor_scale_int" , 0)) + + block.add_32bit_uint(values.get("export_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l1_export_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l2_export_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l3_export_energy_active_int" , 0)) + block.add_32bit_uint(values.get("import_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l1_import_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l2_import_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l3_import_energy_active_int" , 0)) + block.add_16bit_int (values.get("energy_active_scale_int" , 0)) + + block.add_32bit_uint(values.get("export_energy_apparent_int", 0)) + block.add_32bit_uint(values.get("l1_export_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l2_export_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l3_export_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("import_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l1_import_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l2_import_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l3_import_energy_apparent_int" , 0)) + block.add_16bit_int (values.get("energy_apparent_scale_int" , 0)) + + block.add_32bit_uint(values.get("import_energy_reactive_q1_int" , 0)) + block.add_32bit_uint(values.get("l1_import_energy_reactive_q1_int" , 0)) + block.add_32bit_uint(values.get("l2_import_energy_reactive_q1_int" , 0)) + block.add_32bit_uint(values.get("l3_import_energy_reactive_q1_int" , 0)) + block.add_32bit_uint(values.get("import_energy_reactive_q2_int" , 0)) + block.add_32bit_uint(values.get("l1_import_energy_reactive_q2_int" , 0)) + block.add_32bit_uint(values.get("l2_import_energy_reactive_q2_int" , 0)) + block.add_32bit_uint(values.get("l3_import_energy_reactive_q2_int" , 0)) + block.add_32bit_uint(values.get("export_energy_reactive_q3_int" , 0)) + block.add_32bit_uint(values.get("l1_export_energy_reactive_q3_int" , 0)) + block.add_32bit_uint(values.get("l2_export_energy_reactive_q3_int" , 0)) + block.add_32bit_uint(values.get("l3_export_energy_reactive_q3_int" , 0)) + block.add_32bit_uint(values.get("export_energy_reactive_q4_int" , 0)) + block.add_32bit_uint(values.get("l1_export_energy_reactive_q4_int" , 0)) + block.add_32bit_uint(values.get("l2_export_energy_reactive_q4_int" , 0)) + block.add_32bit_uint(values.get("l3_export_energy_reactive_q4_int" , 0)) + block.add_16bit_int (values.get("energy_reactive_scale_int" , 0)) + + block.add_32bit_uint(values.get("events_int" , 0)) + # + + +def setBatteryValues(values, block): + if not values: + block.add_16bit_uint(0) + block.add_16bit_uint(0) + return + + block.add_16bit_uint(1) ## TODO set correct values + block.add_16bit_uint(65) ## TODO set correct values + +def t_update_se7k(ctx, stop, module, device, refresh): + + this_t = threading.currentThread() + logger = logging.getLogger() + + try: + values = module.values(device) + + if not values: + logger.info("no values read from device so discard") + return + + if values.get("energy_total_int") == 0: + logger.info("energy_total_int not set") + if values.get("frequency_int") == 0: + logger.info("power_ac_int not set so discard update") + + block_40000 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + block_40000.add_string("SunS") + block_40000.add_16bit_int(1) + block_40000.add_16bit_int (values.get("C_SunSpec_Length_int", 65)) + block_40000.add_string (values.get("c_manufacturer_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block_40000.add_string (values.get("c_model_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block_40000.add_string ("NOT_IMPLEMENTED.".ljust(16,' ')) + block_40000.add_string (values.get("c_version_str" ,"1234567890123456").ljust(16,' ')) + block_40000.add_string (values.get("c_serialnumber_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block_40000.add_16bit_int (values.get("c_deviceaddress_int" , 0)) + + block_40000.add_16bit_int (values.get("c_sunspec_did_int" , 103)) + block_40000.add_16bit_int (50) + block_40000.add_16bit_uint(values.get("current_int" , 0)) + block_40000.add_16bit_uint(values.get("l1_current_int" , 0)) + block_40000.add_16bit_uint(values.get("l2_current_int" , 0)) + block_40000.add_16bit_uint(values.get("l3_current_int" , 0)) + block_40000.add_16bit_int (values.get("current_scale_int" , 0)) + + block_40000.add_16bit_uint(values.get("l1_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l2_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l3_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l1n_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l2n_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l3n_voltage_int" , 0)) + block_40000.add_16bit_int (values.get("voltage_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_ac_int" , 0)) + block_40000.add_16bit_int (values.get("power_ac_scale_int" , 0)) + + block_40000.add_16bit_uint(values.get("frequency_int" , 0)) + block_40000.add_16bit_int (values.get("frequency_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_apparent_int" , 0)) + block_40000.add_16bit_int (values.get("power_apparent_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_reactive_int" , 0)) + block_40000.add_16bit_int (values.get("power_reactive_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_factor_int" , 0)) + block_40000.add_16bit_int (values.get("power_factor_scale_int" , 0)) + + block_40000.add_32bit_uint(values.get("energy_total_int" , 0)) + block_40000.add_16bit_int (values.get("energy_total_scale_int" , 0)) + + block_40000.add_16bit_uint(values.get("current_dc_int" , 0)) + block_40000.add_16bit_int (values.get("current_dc_scale_int" , 0)) + + block_40000.add_16bit_uint(values.get("voltage_dc_int" , 0)) + block_40000.add_16bit_int (values.get("voltage_dc_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_dc_int" , 0)) + block_40000.add_16bit_int (values.get("power_dc_scale_int" , 0)) + + block_40000.add_16bit_int(0) # 1 dummy word + + block_40000.add_16bit_int(values.get("temperature_int" , 0)) + block_40000.add_16bit_int(values.get("temperature_scale_int" , 0)) + + block_40000.add_16bit_int(0) # 1 dummy word + block_40000.add_16bit_int(0) # 1 dummy word + + block_40000.add_16bit_uint(values.get("status_int" , 0)) + block_40000.add_16bit_uint(values.get("vendor_status_int" , 0)) + + block_40000.add_16bit_uint(values.get("rrcr_state_int" , 0)) + block_40000.add_16bit_int(values.get("active_power_limit_int" , 0)) + block_40000.add_32bit_float(values.get("cosphi" , 0)) + + block_40000.add_string("123456789012345678901234") # 12 dummy worter = 24 Byte + ctx.setValues(3, 40000, block_40000.to_registers()) + + block_40121 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + setMeterValues(values["connected_meters"]["Meter1"],block_40121) + ctx.setValues(3, 40121, block_40121.to_registers()) + + # block_40295 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + # ctx.setValues(3, 40295, block_40295.to_registers()) + # block_40469 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + # ctx.setValues(3, 40469, block_40469.to_registers()) + + # block_57598 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + # ctx.setValues(3, 57598, block_57598.to_registers()) + # block_57854 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + # ctx.setValues(3, 57854, block_57854.to_registers()) + + except Exception as e: + logger.critical(f"{this_t.name}: {e}") + return + return + + + + +def t_update(ctx, SE7K_CTX, stop, module, device, refresh): + + this_t = threading.currentThread() + logger = logging.getLogger() + + while not stop.is_set(): + try: + logger.debug('before t_update_se7k ') + if not t_update_se7k(SE7K_CTX, stop, module, device, refresh): + logger.debug('update not succesful t_update_se7k ') + logger.debug('after t_update_se7k ') + + values = module.values(device) + if not values: + logger.debug(f"{this_t.name}: no new values") + continue + + # use the values from the SE-MTR-3Y-400V-A SE Meter + values = values["connected_meters"]["Meter1"] + + if logger.isEnabledFor(logging.DEBUG): + logger.info("current:"+str(values.get('current_int', 0))) + logger.info(values.get('current_int', 0)*10**values.get('current_scale_int', 0)) + logger.info(values.get('l1_current_int', 0)*10**values.get('current_scale_int', 0)) + logger.info(values.get('l2_current_int', 0)*10**values.get('current_scale_int', 0)) + logger.info(values.get('l3_current_int', 0)*10**values.get('current_scale_int', 0)) + logger.debug(values.get('current_scale_int', 0)) + + logger.debug("voltage_ln:"+str(values.get('voltage_ln_int', 0))) + logger.debug(values.get('voltage_ln_int', 0)*10**values.get('voltage_scale_int', 0)) + logger.debug(values.get('l1n_voltage_int', 0)*10**values.get('voltage_scale_int', 0)) + logger.debug(values.get('l2n_voltage_int', 0)*10**values.get('voltage_scale_int', 0)) + logger.debug(values.get('l3n_voltage_int', 0)*10**values.get('voltage_scale_int', 0)) + logger.debug(values.get('voltage_scale_int', 0)) + + logger.debug("voltage_ll:"+str(values.get('voltage_ll_int', 0))) + logger.debug(values.get('voltage_ll_int', 0)*10**values.get('voltage_scale_int', 0)) + logger.debug(values.get('l12_voltage_int', 0)*10**values.get('voltage_scale_int', 0)) + logger.debug(values.get('l23_voltage_int', 0)*10**values.get('voltage_scale_int', 0)) + logger.debug(values.get('l31_voltage_int', 0)*10**values.get('voltage_scale_int', 0)) + logger.debug(values.get('voltage_scale_int', 0)) + + + logger.info("frequency:"+str(values.get('frequency_int', 0))) + logger.info(values.get('frequency_int', 0)*10**values.get('frequency_scale_int', 0)) + + logger.info("power:"+str(values.get('power_int', 0))) + logger.info(values.get('power_int', 0)*10**values.get('power_scale_int', 0)) + logger.info(values.get('l1_power_int', 0)*10**values.get('power_scale_int', 0)) + logger.info(values.get('l2_power_int', 0)*10**values.get('power_scale_int', 0)) + logger.info(values.get('l3_power_int', 0)*10**values.get('power_scale_int', 0)) + logger.info(values.get('power_scale_int', 0)) + + logger.info("power_apparent:"+str(values.get('power_apparent_int', 0))) + logger.info(values.get('power_apparent_int', 0)*10**values.get('power_apparent_scale_int', 0)) + logger.info(values.get('l1_power_apparent_int', 0)*10**values.get('power_apparent_scale_int', 0)) + logger.info(values.get('l2_power_apparent_int', 0)*10**values.get('power_apparent_scale_int', 0)) + logger.info(values.get('l3_power_apparent_int', 0)*10**values.get('power_apparent_scale_int', 0)) + logger.info(values.get('power_apparent_scale_int', 0)) + + logger.info("power_reactive:"+str(values.get('power_reactive_int', 0))) + logger.info(values.get('power_reactive_int', 0)*10**values.get('power_reactive_scale_int', 0)) + logger.info(values.get('l1_power_reactive_int', 0)*10**values.get('power_reactive_scale_int', 0)) + logger.info(values.get('l2_power_reactive_int', 0)*10**values.get('power_reactive_scale_int', 0)) + logger.info(values.get('l3_power_reactive_int', 0)*10**values.get('power_reactive_scale_int', 0)) + logger.info(values.get('power_reactive_scale_int', 0)) + + logger.info("power_factor:"+str(values.get('power_factor_int', 0))) + logger.info(values.get('power_factor_int', 0)*10**values.get('power_factor_scale_int', 0)) + logger.info(values.get('l1_power_factor_int', 0)*10**values.get('power_factor_scale_int', 0)) + logger.info(values.get('l2_power_factor_int', 0)*10**values.get('power_factor_scale_int', 0)) + logger.info(values.get('l3_power_factor_int', 0)*10**values.get('power_factor_scale_int', 0)) + logger.info(values.get('power_factor_scale_int', 0)) + + logger.debug("export_energy_active:"+str(values.get('export_energy_active_int', 0))) + logger.debug(values.get('export_energy_active_int', 0)*10**values.get('energy_active_scale_int', 0)) + logger.debug(values.get('l1_export_energy_active_int', 0)*10**values.get('energy_active_scale_int', 0)) + logger.debug(values.get('l2_export_energy_active_int', 0)*10**values.get('energy_active_scale_int', 0)) + logger.debug(values.get('l3_export_energy_active_int', 0)*10**values.get('energy_active_scale_int', 0)) + logger.debug(values.get('energy_active_scale_int', 0)) + + logger.debug("import_energy_active:"+str(values.get('import_energy_active_int', 0))) + logger.debug(values.get('import_energy_active_int', 0)*10**values.get('energy_active_scale_int', 0)) + logger.debug(values.get('l1_import_energy_active_int', 0)*10**values.get('energy_active_scale_int', 0)) + logger.debug(values.get('l2_import_energy_active_int', 0)*10**values.get('energy_active_scale_int', 0)) + logger.debug(values.get('l3_import_energy_active_int', 0)*10**values.get('energy_active_scale_int', 0)) + logger.debug(values.get('energy_active_scale_int', 0)) + + logger.debug("import_energy_apparent:"+str(values.get('import_energy_apparent_int', 0))) + logger.debug(values.get('import_energy_apparent_int', 0)*10**values.get('energy_apparent_scale_int', 0)) + logger.debug(values.get('l1_import_energy_apparent_int', 0)*10**values.get('energy_apparent_scale_int', 0)) + logger.debug(values.get('l2_import_energy_apparent_int', 0)*10**values.get('energy_apparent_scale_int', 0)) + logger.debug(values.get('l3_import_energy_apparent_int', 0)*10**values.get('energy_apparent_scale_int', 0)) + logger.debug(values.get('energy_apparent_scale_int', 0)) + + + + block_0 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_0.add_32bit_int(int(values.get('l1n_voltage_int', 0)/10)) # l1-n voltage * 10 + block_0.add_32bit_int(int(values.get('l2n_voltage_int', 0)/10)) # l2-n voltage + block_0.add_32bit_int(int(values.get('l3n_voltage_int', 0)/10)) # l3-n voltage + block_0.add_32bit_int(int(values.get('l12_voltage_int', 0)/10)) # l1-l2 voltage + block_0.add_32bit_int(int(values.get('l23_voltage_int', 0)/10)) # l2-l3 voltage + block_0.add_32bit_int(int(values.get('l31_voltage_int', 0)/10)) # l3-l1 voltage + block_0.add_32bit_int(values.get('l1_current_int', 0)*100) # current l1 * 1000 + block_0.add_32bit_int(values.get('l2_current_int', 0)*100) # current l2 + block_0.add_32bit_int(values.get('l3_current_int', 0)*100) # current l3 + block_0.add_32bit_int(values.get('l1_power_int', 0)*-10) # power l1 *10 + block_0.add_32bit_int(values.get('l2_power_int', 0)*-10) # power l2 + block_0.add_32bit_int(values.get('l3_power_int', 0)*-10) # power l3 + block_0.add_32bit_int(values.get('l1_power_apparent_int', 0)*-10) # apparent power l1 *10 + block_0.add_32bit_int(values.get('l2_power_apparent_int', 0)*-10) # apparent power l2 + block_0.add_32bit_int(values.get('l3_power_apparent_int', 0)*-10) # apparent power l3 + block_0.add_32bit_int(values.get('l1_power_reactive_int', 0)*-10) # reactive power l1 *10 + block_0.add_32bit_int(values.get('l2_power_reactive_int', 0)*-10) # reactive power l2 + block_0.add_32bit_int(values.get('l3_power_reactive_int', 0)*-10) # reactive power l3 + block_0.add_32bit_int(int(values.get('voltage_ln_int', 0)/10)) # l-n voltage *10 + block_0.add_32bit_int(int(values.get('voltage_ll_int', 0)/10)) # l-l voltage + block_0.add_32bit_int(values.get('power_int', 0)*-10) # total power *10 + block_0.add_32bit_int(values.get('power_apparent_int', 0)*-10) # total apparent power + block_0.add_32bit_int(values.get('power_reactive_int', 0)*-10) # total reactive power + block_0.add_16bit_int(int(values.get('l1_power_factor_int', 0)/10)) # power factor l1 *1000 + block_0.add_16bit_int(int(values.get('l2_power_factor_int', 0)/10)) # power factor l2 + block_0.add_16bit_int(int(values.get('l3_power_factor_int', 0)/10)) # power factor l3 + block_0.add_16bit_int(int(values.get('power_factor_int', 0)/10)) # power factor + block_0.add_16bit_int(0) # Value –1 correspond to L1-L3-L2 sequence, value 0 correspond to L1-L2-L3 sequence (this value is meaningful only in case of 3-phase systems) + + block_0.add_16bit_uint(int(values.get('frequency_int', 0)/10)) # line frequency *10 + + block_0.add_32bit_int(int(values.get('import_energy_active_int', 0)/100)) # imported active energy + block_0.add_32bit_int(int(values.get('import_energy_apparent_int', 0)/100)) # imported active energy + block_0.add_32bit_int(56) # demand power + block_0.add_32bit_int(58) # maximum demand power + block_0.add_32bit_int(int(values.get('import_energy_active_int', 0)/100)) # imported active energy + block_0.add_32bit_int(int(values.get('import_energy_apparent_int', 0)/100)) # imported active energy + block_0.add_32bit_int(int(values.get('l1_import_energy_active_int', 0)/100)) # imported active energy l1 + block_0.add_32bit_int(int(values.get('l2_import_energy_active_int', 0)/100)) # imported active energy l2 + block_0.add_32bit_int(int(values.get('l3_import_energy_active_int', 0)/100)) # imported active energy l3 + block_0.add_32bit_int(10) # total active energy Tarif 1 + block_0.add_32bit_int(20) # total active energy Tarif 2 + block_0.add_32bit_int(30) # total active energy Tarif 3 + block_0.add_32bit_int(40) # total active energy Tarif 4 + block_0.add_32bit_int(int(values.get('export_energy_active_int', 0)/100)) # total exported active energy non-reset /100) + block_0.add_32bit_int(int(values.get('export_energy_apparent_int', 0)/100)) # imported active energy non-reset + block_0.add_32bit_int(2400) # hour *100 + block_0.add_32bit_int(11) # total apparent energy Tarif 1 *10 + block_0.add_32bit_int(22) # total apparent energy Tarif 2 + block_0.add_32bit_int(33) # total apparent energy Tarif 3 + block_0.add_32bit_int(44) # total apparent energy Tarif 4 + block_0.add_32bit_int(118) # apparent demand power + block_0.add_32bit_int(120) # apparent demand power max + block_0.add_32bit_int(122) # DMD A max *10 + ctx.setValues(3, 0, block_0.to_registers()) + ctx.setValues(4, 0, block_0.to_registers()) + + block_254 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_254.add_32bit_int(2400) # hour *100 *100 + block_254.add_32bit_int(256) # unused *100 + block_254.add_32bit_int(int(values.get('voltage_ln_int', 0)/10)) # l-n voltage *10 + block_254.add_32bit_int(int(values.get('voltage_ll_int', 0)/10)) # l-l voltage + block_254.add_32bit_int(values.get('power_int', 0)*-10) # total power *10 + block_254.add_32bit_int(values.get('power_apparent_int', 0)*-10) # total apparent power + block_254.add_32bit_int(values.get('power_reactive_int', 0)*-10) # total reactive power + block_254.add_32bit_int(int(values.get('power_factor_int', 0)/10)) # power factor + block_254.add_32bit_int(0) # Value –1 correspond to L1-L3-L2 sequence, value 0 correspond to L1-L2-L3 sequence (this value is meaningful only in case of 3-phase systems) + block_254.add_32bit_int(int(values.get('frequency_int', 0)/10)) # line frequency *10 + block_254.add_32bit_int(int(values.get('import_energy_active_int', 0)/100)) # imported active energy + block_254.add_32bit_int(int(values.get('import_energy_apparent_int', 0)/100)) # imported active energy + block_254.add_32bit_int(int(values.get('export_energy_active_int', 0)/100)) # total exported active energy non-reset /100) + block_254.add_32bit_int(int(values.get('export_energy_apparent_int', 0)/100)) # imported active energy non-reset + block_254.add_32bit_int(56) # demand power + block_254.add_32bit_int(58) # maximum demand power + + + + + block_254.add_32bit_int(int(values.get('l12_voltage_int', 0)/10)) # l1-l2 voltage + block_254.add_32bit_int(int(values.get('l1n_voltage_int', 0)/10)) # l1-n voltage * 10 + block_254.add_32bit_int(values.get('l1_current_int', 0)*100) # current l1 * 1000 + block_254.add_32bit_int(values.get('l1_power_int', 0)*-10) # power l1 *10 + block_254.add_32bit_int(values.get('l1_power_apparent_int', 0)*-10) # apparent power l1 *10 + block_254.add_32bit_int(values.get('l1_power_reactive_int', 0)*-10) # reactive power l1 *10 + block_254.add_32bit_int(int(values.get('l1_power_factor_int', 0)/10)) # power factor l1 *1000 + + block_254.add_32bit_int(int(values.get('l23_voltage_int', 0)/10)) # l2-l3 voltage + block_254.add_32bit_int(int(values.get('l2n_voltage_int', 0)/10)) # l2-n voltage + block_254.add_32bit_int(values.get('l2_current_int', 0)*100) # current l2 + block_254.add_32bit_int(values.get('l2_power_int', 0)*-10) # power l2 + block_254.add_32bit_int(values.get('l2_power_apparent_int', 0)*-10) # apparent power l2 + block_254.add_32bit_int(values.get('l2_power_reactive_int', 0)*-10) # reactive power l2 + block_254.add_32bit_int(int(values.get('l2_power_factor_int', 0)/10)) # power factor l2 + + block_254.add_32bit_int(int(values.get('l31_voltage_int', 0)/10)) # l3-l1 voltage + block_254.add_32bit_int(int(values.get('l3n_voltage_int', 0)/10)) # l3-n voltage + block_254.add_32bit_int(values.get('l3_current_int', 0)*100) # current l3 + block_254.add_32bit_int(values.get('l3_power_int', 0)*-10) # power l3 + block_254.add_32bit_int(values.get('l3_power_apparent_int', 0)*-10) # apparent power l3 + block_254.add_32bit_int(values.get('l3_power_reactive_int', 0)*-10) # reactive power l3 + block_254.add_32bit_int(int(values.get('l3_power_factor_int', 0)/10)) # power factor l3 + + block_254.add_32bit_int(0) # Value –1 correspond to L1-L3-L2 sequence, value 0 correspond to L1-L2-L3 sequence (this value is meaningful only in case of 3-phase systems) + + block_254.add_32bit_int(int(values.get('import_energy_active_int', 0)/100)) # imported active energy + block_254.add_32bit_int(int(values.get('import_energy_apparent_int', 0)/100)) # imported active energy + block_254.add_32bit_int(int(values.get('l1_import_energy_active_int', 0)/100)) # imported active energy l1 + block_254.add_32bit_int(int(values.get('l2_import_energy_active_int', 0)/100)) # imported active energy l2 + block_254.add_32bit_int(int(values.get('l3_import_energy_active_int', 0)/100)) # imported active energy l3 + block_254.add_32bit_int(10) # total active energy Tarif 1 + block_254.add_32bit_int(20) # total active energy Tarif 2 + block_254.add_32bit_int(30) # total active energy Tarif 3 + block_254.add_32bit_int(40) # total active energy Tarif 4 + block_254.add_32bit_int(346) # unused *100 + block_254.add_32bit_int(348) # unused *100 + block_254.add_32bit_int(350) # unused *100 + block_254.add_32bit_int(352) # unused *100 + block_254.add_32bit_int(11) # total apparent energy Tarif 1 *10 + block_254.add_32bit_int(22) # total apparent energy Tarif 2 + block_254.add_32bit_int(33) # total apparent energy Tarif 3 + block_254.add_32bit_int(44) # total apparent energy Tarif 4 + block_254.add_32bit_int(262) # unused *100 + block_254.add_32bit_int(264) # unused *100 + block_254.add_32bit_int(266) # unused *100 + block_254.add_32bit_int(268) # unused *100 + block_254.add_32bit_int(270) # unused *100 + block_254.add_32bit_int(272) # unused *100 + block_254.add_32bit_int(274) # unused *100 + block_254.add_32bit_int(276) # unused *100 + block_254.add_32bit_int(118) # apparent demand power + block_254.add_32bit_int(120) # apparent demand power max + block_254.add_32bit_int(122) # DMD A max *10 + ctx.setValues(3, 254, block_254.to_registers()) + ctx.setValues(4, 254, block_254.to_registers()) + + ## unused values + # "energy_reactive" # total reactive energy + # "l1_export_energy_active", 0)) # exported energy l1 + # "l2_export_energy_active", 0)) # exported energy l2 + # "l3_export_energy_active", 0)) # exported energy l3 + # "l1_energy_reactive", 0)) # reactive energy l1 + # "l2_energy_reactive", 0)) # reactive energy l2 + # "l3_energy_reactive", 0)) # reactive energy l3 + # "l1_energy_apparent", 0)) # apparent energy l1 + # "l2_energy_apparent", 0)) # apparent energy l2 + # "l3_energy_apparent", 0)) # apparent energy l3 + # "minimum_demand_power_active", 0)) # minimum demand power + # "l1_demand_power_active", 0)) # demand power l1 + # "l2_demand_power_active", 0)) # demand power l2 + # "l3_demand_power_active", 0)) # demand power l3 + except Exception as e: + logger.critical(f"{this_t.name}: {e}") + finally: + time.sleep(0.6) + + +if __name__ == "__main__": + argparser = argparse.ArgumentParser() + argparser.add_argument("-c", "--config", type=str, default="semp-tcp.conf") + argparser.add_argument("-v", "--verbose", action="store_true", default=False) + args = argparser.parse_args() + + default_config = { + "server": { + "address": "0.0.0.0", + "port": 502, + "framer": "socket", + "log_level": "INFO", + "meters": 'Meter1' + }, + "meters": { + "dst_address": 2, + "type": "generic", + "ct_current": 5, + "ct_inverted": 0, + "phase_offset": 120, + "serial_number": 987654, + "refresh_rate": 5 + } + } + + confparser = configparser.ConfigParser() + confparser.read(args.config) + + if not confparser.has_section("server"): + confparser["server"] = default_config["server"] + + log_handler = logging.StreamHandler(sys.stdout) + log_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")) + + logger = logging.getLogger() + logger.setLevel(getattr(logging, confparser["server"].get("log_level", fallback=default_config["server"]["log_level"]).upper())) + logger.addHandler(log_handler) + + if args.verbose: + logger.setLevel(logging.DEBUG) + + slaves = {} + threads = [] + thread_stops = [] + + try: + if confparser.has_option("server", "meters"): + meters = [m.strip() for m in confparser["server"].get("meters", fallback=default_config["server"]["meters"]).split(',')] + + for meter in meters: + address = confparser[meter].getint("dst_address", fallback=default_config["meters"]["dst_address"]) + meter_type = confparser[meter].get("type", fallback=default_config["meters"]["type"]) + meter_module = importlib.import_module(f"devices.{meter_type}") + meter_device = meter_module.device(confparser[meter]) + + EM24_slave_ctx = EM24SlaveContext() + SE7K_slave_ctx = ModbusSlaveContext() + + # block_11 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + # block_11.add_16bit_int(1648) + # EM24_slave_ctx.setValues(3, 11, block_11.to_registers()) + # EM24_slave_ctx.setValues(4, 11, block_11.to_registers()) + + block_0 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_0.add_32bit_int(1234) + EM24_slave_ctx.setValues(3, 0, block_0.to_registers()) + EM24_slave_ctx.setValues(4, 0, block_0.to_registers()) + + block_770 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_770.add_16bit_int(4126) # Version and revision measurment module + block_770.add_16bit_int(68) # + block_770.add_16bit_int(4127) # Version and revision communication module + block_770.add_16bit_int(67) # + block_770.add_16bit_int(0) # Current tariff + EM24_slave_ctx.setValues(3, 770, block_770.to_registers()) + EM24_slave_ctx.setValues(4, 770, block_770.to_registers()) + + block_848 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_848.add_16bit_int(4128) # Measurement module’s firmware CRC + EM24_slave_ctx.setValues(3, 848, block_848.to_registers()) + EM24_slave_ctx.setValues(4, 848, block_848.to_registers()) + + block_20480 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_20480.add_string("MB24DINAV23XE1X") + EM24_slave_ctx.setValues(3, 20480, block_20480.to_registers()) + EM24_slave_ctx.setValues(4, 20480, block_20480.to_registers()) + + block_41216 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_41216.add_16bit_int(3) # Front selector status + EM24_slave_ctx.setValues(3, 41216, block_41216.to_registers()) + EM24_slave_ctx.setValues(4, 41216, block_41216.to_registers()) + + block_4096 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_4096.add_16bit_int(9999) # PASSWORD + block_4096.add_16bit_int(0) # unused + block_4096.add_16bit_int(0) # Measuring system + block_4096.add_32bit_int(10) # Current transformer ratio + block_4096.add_32bit_int(10) # Voltage transformer ratio + block_4096.add_16bit_int(1) # unused + block_4096.add_16bit_int(2) # unused + block_4096.add_16bit_int(3) # unused + block_4096.add_16bit_int(4) # unused + block_4096.add_16bit_int(5) # unused + block_4096.add_16bit_int(6) # unused + block_4096.add_16bit_int(7) # unused + block_4096.add_16bit_int(8) # unused + block_4096.add_16bit_int(9) # unused + block_4096.add_32bit_int(15) # Interval time + EM24_slave_ctx.setValues(3, 4096, block_4096.to_registers()) + EM24_slave_ctx.setValues(4, 4096, block_4096.to_registers()) + + block_4360 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_4360.add_16bit_int(2) # PASSWORD + block_4360.add_16bit_int(2) # PASSWORD + EM24_slave_ctx.setValues(3, 4360, block_4360.to_registers()) + EM24_slave_ctx.setValues(4, 4360, block_4360.to_registers()) + + block_40960 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_40960.add_16bit_int(1) # Type of application + block_40960.add_16bit_int(3) # Default page for selector position “LOCK” + block_40960.add_16bit_int(1) # Default page for selector position “1” + block_40960.add_16bit_int(3) # Default page for selector position “2” + block_40960.add_16bit_int(3) # Default page for selector position “kvarh” + block_40960.add_16bit_int(1) # ID code of user 1 + block_40960.add_16bit_int(2) # ID code of user 2 + block_40960.add_16bit_int(3) # ID code of user 3 + EM24_slave_ctx.setValues(3, 40960, block_40960.to_registers()) + EM24_slave_ctx.setValues(4, 40960, block_40960.to_registers()) + + update_t_stop = threading.Event() + update_t = threading.Thread( + target=t_update, + name=f"t_update_{address}", + args=( + EM24_slave_ctx, + SE7K_slave_ctx, + update_t_stop, + meter_module, + meter_device, + confparser[meter].getfloat("refresh_rate", fallback=default_config["meters"]["refresh_rate"]) + ) + ) + + threads.append(update_t) + thread_stops.append(update_t_stop) + + slaves.update({1: EM24_slave_ctx}) + slaves.update({2: SE7K_slave_ctx}) + logger.info(f"Created {update_t}: {meter} {meter_type} {meter_device}") + + if not slaves: + logger.warning(f"No meters defined in {args.config}") + + config_framer = confparser["server"].get("framer", fallback=default_config["server"]["framer"]) + framer = False + + if config_framer == "socket": + framer = ModbusSocketFramer + elif config_framer == "rtu": + framer = ModbusRtuFramer + + identity = ModbusDeviceIdentification() + server_ctx = ModbusServerContext(slaves=slaves, single=False) + + time.sleep(1) + + for t in threads: + t.start() + logger.info(f"Starting {t}") + + StartMyTcpServer( + server_ctx, + framer=framer, + identity=identity, + address=( + confparser["server"].get("address", fallback=default_config["server"]["address"]), + confparser["server"].getint("port", fallback=default_config["server"]["port"]) + ) + ) + except KeyboardInterrupt: + pass + finally: + for t_stop in thread_stops: + t_stop.set() + for t in threads: + t.join() diff --git a/SE7K-EM24-proxy-tcp.sh b/SE7K-EM24-proxy-tcp.sh new file mode 100644 index 0000000..7df619b --- /dev/null +++ b/SE7K-EM24-proxy-tcp.sh @@ -0,0 +1,2 @@ +#!/bin/bash +python3 SE7K-proxy-tcp.py -c SE-MTR-3Y-400V-A.conf diff --git a/SE7K-proxy-tcp.cmd b/SE7K-proxy-tcp.cmd new file mode 100644 index 0000000..db0d585 --- /dev/null +++ b/SE7K-proxy-tcp.cmd @@ -0,0 +1,2 @@ +echo use -v for more information +python SE7K-proxy-tcp.py -c SE7K.conf %* diff --git a/SE7K-proxy-tcp.d.sh b/SE7K-proxy-tcp.d.sh new file mode 100755 index 0000000..ddf012b --- /dev/null +++ b/SE7K-proxy-tcp.d.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set +x +meinpfad=$(dirname $0) +file=/var/log/modbus-proxy.log +logfile=$file +[ -e $file ] && [ $(stat --printf '%s' "$file") -gt 104857600 ] && rm "$file" +file=/var/log/modbus-proxy.err +errfile=$file +[ -e $file ] && [ $(stat --printf '%s' "$file") -gt 104857600 ] && rm "$file" +nohup python3 $meinpfad/SE7K-proxy-tcp.py -c $meinpfad/SE7K.conf 2>>$errfile >>$logfile & + diff --git a/SE7K-proxy-tcp.py b/SE7K-proxy-tcp.py new file mode 100644 index 0000000..f38d56a --- /dev/null +++ b/SE7K-proxy-tcp.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 + +import argparse +import configparser +import importlib +import logging +import sys +import threading +import time + +from pymodbus.server.sync import StartTcpServer +from pymodbus.constants import Endian +from pymodbus.device import ModbusDeviceIdentification +from pymodbus.transaction import ModbusSocketFramer +from pymodbus.transaction import ModbusRtuFramer +from pymodbus.datastore import ModbusSlaveContext +from pymodbus.datastore import ModbusServerContext +from pymodbus.payload import BinaryPayloadBuilder + + +class EM24SlaveContext(ModbusSlaveContext): + def getValues(self, fx, address, count=1): + #if (address == 11 and count==1): + # print("Return Gavazzi Model number 1648") + # return [1648] + return super().getValues(fx, address, count) + + +def setMeterValues(values, block): + if not values: + block.add_16bit_uint(0) + block.add_16bit_uint(0) + return + + block.add_16bit_uint(1) + block.add_16bit_uint(65) + block.add_string (values.get("c_manufacturer_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block.add_string (values.get("c_model_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block.add_string (values.get("c_option_str" ,"1234567890123456").ljust(16,' ')) + block.add_string (values.get("c_version_str" ,"1234567890123456").ljust(16,' ')) + block.add_string (values.get("c_serialnumber_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block.add_16bit_int (values.get("c_deviceaddress_int" , 0)) + + block.add_16bit_int (values.get("c_sunspec_did_int" , 103)) + block.add_16bit_int (values.get("c_sunspec_length_int", 50)) + block.add_16bit_uint(values.get("current_int" , 0)) + block.add_16bit_uint(values.get("l1_current_int" , 0)) + block.add_16bit_uint(values.get("l2_current_int" , 0)) + block.add_16bit_uint(values.get("l3_current_int" , 0)) + block.add_16bit_int (values.get("current_scale_int" , 0)) + + block.add_16bit_uint(values.get("voltage_ln_int" , 0)) + block.add_16bit_uint(values.get("l1n_voltage_int" , 0)) + block.add_16bit_uint(values.get("l2n_voltage_int" , 0)) + block.add_16bit_uint(values.get("l3n_voltage_int" , 0)) + block.add_16bit_uint(values.get("voltage_ll_int" , 0)) + block.add_16bit_uint(values.get("l1n_voltage_int" , 0)) + block.add_16bit_uint(values.get("l2n_voltage_int" , 0)) + block.add_16bit_uint(values.get("l3n_voltage_int" , 0)) + block.add_16bit_int (values.get("voltage_scale_int" , 0)) + + block.add_16bit_uint(values.get("frequency_int" , 0)) + block.add_16bit_int (values.get("frequency_scale_int" , 0)) + + block.add_16bit_int(values.get("power_int" , 0)) + block.add_16bit_int(values.get("l1_power_int" , 0)) + block.add_16bit_int(values.get("l2_power_int" , 0)) + block.add_16bit_int(values.get("l3_power_int" , 0)) + block.add_16bit_int (values.get("power_scale_int" , 0)) + + block.add_16bit_int(values.get("power_apparent_int" , 0)) + block.add_16bit_int(values.get("l1_power_apparent_int" , 0)) + block.add_16bit_int(values.get("l2_power_apparent_int" , 0)) + block.add_16bit_int(values.get("l3_power_apparent_int" , 0)) + block.add_16bit_int (values.get("power_apparent_scale_int" , 0)) + + block.add_16bit_int(values.get("power_reactive_int" , 0)) + block.add_16bit_int(values.get("l1_power_reactive_int" , 0)) + block.add_16bit_int(values.get("l2_power_reactive_int" , 0)) + block.add_16bit_int(values.get("l3_power_reactive_int" , 0)) + block.add_16bit_int (values.get("power_reactive_scale_int" , 0)) + + block.add_16bit_int(values.get("power_factor_int" , 0)) + block.add_16bit_int(values.get("l1_power_factor_int" , 0)) + block.add_16bit_int(values.get("l2_power_factor_int" , 0)) + block.add_16bit_int(values.get("l3_power_factor_int" , 0)) + block.add_16bit_int (values.get("power_factor_scale_int" , 0)) + + block.add_32bit_uint(values.get("export_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l1_export_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l2_export_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l3_export_energy_active_int" , 0)) + block.add_32bit_uint(values.get("import_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l1_import_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l2_import_energy_active_int" , 0)) + block.add_32bit_uint(values.get("l3_import_energy_active_int" , 0)) + block.add_16bit_int (values.get("energy_active_scale_int" , 0)) + + block.add_32bit_uint(values.get("export_energy_apparent_int", 0)) + block.add_32bit_uint(values.get("l1_export_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l2_export_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l3_export_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("import_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l1_import_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l2_import_energy_apparent_int" , 0)) + block.add_32bit_uint(values.get("l3_import_energy_apparent_int" , 0)) + block.add_16bit_int (values.get("energy_apparent_scale_int" , 0)) + + block.add_32bit_uint(values.get("import_energy_reactive_q1_int" , 0)) + block.add_32bit_uint(values.get("l1_import_energy_reactive_q1_int" , 0)) + block.add_32bit_uint(values.get("l2_import_energy_reactive_q1_int" , 0)) + block.add_32bit_uint(values.get("l3_import_energy_reactive_q1_int" , 0)) + block.add_32bit_uint(values.get("import_energy_reactive_q2_int" , 0)) + block.add_32bit_uint(values.get("l1_import_energy_reactive_q2_int" , 0)) + block.add_32bit_uint(values.get("l2_import_energy_reactive_q2_int" , 0)) + block.add_32bit_uint(values.get("l3_import_energy_reactive_q2_int" , 0)) + block.add_32bit_uint(values.get("export_energy_reactive_q3_int" , 0)) + block.add_32bit_uint(values.get("l1_export_energy_reactive_q3_int" , 0)) + block.add_32bit_uint(values.get("l2_export_energy_reactive_q3_int" , 0)) + block.add_32bit_uint(values.get("l3_export_energy_reactive_q3_int" , 0)) + block.add_32bit_uint(values.get("export_energy_reactive_q4_int" , 0)) + block.add_32bit_uint(values.get("l1_export_energy_reactive_q4_int" , 0)) + block.add_32bit_uint(values.get("l2_export_energy_reactive_q4_int" , 0)) + block.add_32bit_uint(values.get("l3_export_energy_reactive_q4_int" , 0)) + block.add_16bit_int (values.get("energy_reactive_scale_int" , 0)) + + block.add_32bit_uint(values.get("events_int" , 0)) + # + + +def setBatteryValues(values, block): + if not values: + block.add_16bit_uint(0) + block.add_16bit_uint(0) + return + + block.add_16bit_uint(1) ## TODO set correct values + block.add_16bit_uint(65) ## TODO set correct values + +def t_update(ctx, stop, module, device, refresh): + + this_t = threading.currentThread() + logger = logging.getLogger() + + while not stop.is_set(): + try: + values = module.values(device) + + if not values: + logger.debug(f"{this_t.name}: no new values") + continue + + block_40000 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + block_40000.add_string("SunS") + block_40000.add_16bit_int(1) + block_40000.add_16bit_int (values.get("C_SunSpec_Length_int", 65)) + block_40000.add_string (values.get("c_manufacturer_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block_40000.add_string (values.get("c_model_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block_40000.add_string ( "NOT_IMPLEMENTED.".ljust(16,' ')) + block_40000.add_string (values.get("c_version_str" ,"1234567890123456").ljust(16,' ')) + block_40000.add_string (values.get("c_serialnumber_str" ,"12345678901234567890123456789012").ljust(32,' ')) + block_40000.add_16bit_int (values.get("c_deviceaddress_int" , 0)) + + block_40000.add_16bit_int (values.get("c_sunspec_did_int" , 103)) + block_40000.add_16bit_int (50) + block_40000.add_16bit_uint(values.get("current_int" , 0)) + block_40000.add_16bit_uint(values.get("l1_current_int" , 0)) + block_40000.add_16bit_uint(values.get("l2_current_int" , 0)) + block_40000.add_16bit_uint(values.get("l3_current_int" , 0)) + block_40000.add_16bit_int (values.get("current_scale_int" , 0)) + + block_40000.add_16bit_uint(values.get("l1_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l2_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l3_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l1n_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l2n_voltage_int" , 0)) + block_40000.add_16bit_uint(values.get("l3n_voltage_int" , 0)) + block_40000.add_16bit_int (values.get("voltage_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_ac_int" , 0)) + block_40000.add_16bit_int (values.get("power_ac_scale_int" , 0)) + + block_40000.add_16bit_uint(values.get("frequency_int" , 0)) + block_40000.add_16bit_int (values.get("frequency_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_apparent_int" , 0)) + block_40000.add_16bit_int (values.get("power_apparent_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_reactive_int" , 0)) + block_40000.add_16bit_int (values.get("power_reactive_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_factor_int" , 0)) + block_40000.add_16bit_int (values.get("power_factor_scale_int" , 0)) + + block_40000.add_32bit_uint(values.get("energy_total_int" , 0)) + block_40000.add_16bit_int (values.get("energy_total_scale_int" , 0)) + + block_40000.add_16bit_uint(values.get("current_dc_int" , 0)) + block_40000.add_16bit_int (values.get("current_dc_scale_int" , 0)) + + block_40000.add_16bit_uint(values.get("voltage_dc_int" , 0)) + block_40000.add_16bit_int (values.get("voltage_dc_scale_int" , 0)) + + block_40000.add_16bit_int(values.get("power_dc_int" , 0)) + block_40000.add_16bit_int (values.get("power_dc_scale_int" , 0)) + + block_40000.add_16bit_int(0) # 1 dummy word + + block_40000.add_16bit_int(values.get("temperature_int" , 0)) + block_40000.add_16bit_int(values.get("temperature_scale_int" , 0)) + + block_40000.add_16bit_int(0) # 1 dummy word + block_40000.add_16bit_int(0) # 1 dummy word + + block_40000.add_16bit_uint(values.get("status_int" , 0)) + block_40000.add_16bit_uint(values.get("vendor_status_int" , 0)) + + block_40000.add_16bit_uint(values.get("rrcr_state_int" , 0)) + block_40000.add_16bit_int(values.get("active_power_limit_int" , 0)) + block_40000.add_32bit_float(values.get("cosphi" , 0)) + + block_40000.add_string("123456789012345678901234") # 12 dummy worter = 24 Byte + ctx.setValues(3, 40000, block_40000.to_registers()) + + block_40121 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + setMeterValues(values["connected_meters"]["Meter1"],block_40121) + ctx.setValues(3, 40121, block_40121.to_registers()) + + # block_40295 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + # ctx.setValues(3, 40295, block_40295.to_registers()) + # block_40469 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + # ctx.setValues(3, 40469, block_40469.to_registers()) + + # block_57598 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + # ctx.setValues(3, 57598, block_57598.to_registers()) + # block_57854 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Big) + # ctx.setValues(3, 57854, block_57854.to_registers()) + except Exception as e: + logger.critical(f"{this_t.name}: {e}") + finally: + time.sleep(refresh) + + +if __name__ == "__main__": + argparser = argparse.ArgumentParser() + argparser.add_argument("-c", "--config", type=str, default="semp-tcp.conf") + argparser.add_argument("-v", "--verbose", action="store_true", default=False) + args = argparser.parse_args() + + default_config = { + "server": { + "address": "0.0.0.0", + "port": 502, + "framer": "socket", + "log_level": "INFO", + "meters": 'Meter1' + }, + "meters": { + "dst_address": 2, + "type": "generic", + "ct_current": 5, + "ct_inverted": 0, + "phase_offset": 120, + "serial_number": 987654, + "refresh_rate": 2 + } + } + + confparser = configparser.ConfigParser() + confparser.read(args.config) + + if not confparser.has_section("server"): + confparser["server"] = default_config["server"] + + log_handler = logging.StreamHandler(sys.stdout) + log_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")) + + logger = logging.getLogger() + logger.setLevel(getattr(logging, confparser["server"].get("log_level", fallback=default_config["server"]["log_level"]).upper())) + logger.addHandler(log_handler) + + if args.verbose: + logger.setLevel(logging.DEBUG) + + slaves = {} + threads = [] + thread_stops = [] + + try: + if confparser.has_option("server", "meters"): + meters = [m.strip() for m in confparser["server"].get("meters", fallback=default_config["server"]["meters"]).split(',')] + + for meter in meters: + address = confparser[meter].getint("dst_address", fallback=default_config["meters"]["dst_address"]) + meter_type = confparser[meter].get("type", fallback=default_config["meters"]["type"]) + meter_module = importlib.import_module(f"devices.{meter_type}") + meter_device = meter_module.device(confparser[meter]) + + #slave_ctx = EM24SlaveContext() + slave_ctx = ModbusSlaveContext() + + block_40000 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + slave_ctx.setValues(3, 40000, block_40000.to_registers()) + + update_t_stop = threading.Event() + update_t = threading.Thread( + target=t_update, + name=f"t_update_{address}", + args=( + slave_ctx, + update_t_stop, + meter_module, + meter_device, + confparser[meter].getfloat("refresh_rate", fallback=default_config["meters"]["refresh_rate"]) + ) + ) + + threads.append(update_t) + thread_stops.append(update_t_stop) + + slaves.update({address: slave_ctx}) + logger.info(f"Created {update_t}: {meter} {meter_type} {meter_device}") + + if not slaves: + logger.warning(f"No meters defined in {args.config}") + + config_framer = confparser["server"].get("framer", fallback=default_config["server"]["framer"]) + framer = False + + if config_framer == "socket": + framer = ModbusSocketFramer + elif config_framer == "rtu": + framer = ModbusRtuFramer + + identity = ModbusDeviceIdentification() + server_ctx = ModbusServerContext(slaves=slaves, single=False) + + time.sleep(1) + + for t in threads: + t.start() + logger.info(f"Starting {t}") + + StartTcpServer( + server_ctx, + framer=framer, + identity=identity, + address=( + confparser["server"].get("address", fallback=default_config["server"]["address"]), + confparser["server"].getint("port", fallback=default_config["server"]["port"]) + ) + ) + except KeyboardInterrupt: + pass + finally: + for t_stop in thread_stops: + t_stop.set() + for t in threads: + t.join() diff --git a/SE7K-proxy-tcp.sh b/SE7K-proxy-tcp.sh new file mode 100755 index 0000000..5a914d3 --- /dev/null +++ b/SE7K-proxy-tcp.sh @@ -0,0 +1,3 @@ +#!/bin/bash +python3 SE7K-proxy-tcp.py -c SE7K.conf + diff --git a/SE7K.conf b/SE7K.conf new file mode 100644 index 0000000..868b093 --- /dev/null +++ b/SE7K.conf @@ -0,0 +1,66 @@ +[server] +# Serving IP address. +# optional, default: all interfaces +address=0.0.0.0 +# ip-address=0.0.0.0 +# ip_address=0.0.0.0 + +# Serving port. +# optional, default: 5502 +port = 502 + +# Modbus frame type, set to rtu for Modbus RTU over TCP +# optional, default: socket +#framer = socket + +# Logging level, CRITICAL, ERROR, WARNING, INFO, DEBUG +# optional, default: INFO +log_level = INFO + +# Masqueraded meters, comma separated. +# optional, default: '' +meters = SE7K + +[SE7K] +# the solarage inverter should have an SE-MTR-3Y-400V-A meter attached +type=solaredge-inverter +host=raspberrypi.fritz.box +port=502 +src_address=2 +dst_address=2 + + +# Meters defined in [server] need a config section, one per meter. +# Depending on the type of meter that is to be masqueraded, you can +# define a number of generic and type specific variables. + +# Modbus address of the meter as defined in the SolarEdge inverter. +# This value needs to be unique. +# optional, default: 2 +#dst_address = 2 + +# Source meter type, which corresponds to a script in /devices. +# The generic.py device returns null values. +# optional, default: generic +#type = generic + +# Masqueraded serial number. +# Need not be correct, must be unique, must be an integer. +# optional, default: 987654 +#serial_number = 987654 + +# Current transformer amperage rating. +# optional, default: 5 +#ct_current = 50 + +# Current transformer direction inversion, set to 1 if required. +# optional, default: 0 +#ct_inverted = 0 + +# Offset between phases, set to 0, 90, 120 or 180. +# optional, default: 0 +#phase_offset = 120 + +# Number of seconds between value refreshes. +# optional, default: 5 +refresh_rate = 0.5 diff --git a/devices/solaredge-inverter.py b/devices/solaredge-inverter.py new file mode 100644 index 0000000..63be4c4 --- /dev/null +++ b/devices/solaredge-inverter.py @@ -0,0 +1,157 @@ +import logging +import re +import solaredge_modbus + + +def device(config): + + # Configuration parameters: + # + # timeout seconds to wait for a response, default: 1 + # retries number of retries, default: 3 + # unit modbus address, default: 1 + # + # For Modbus TCP: + # host ip or hostname + # port modbus tcp port + # + # For Modbus RTU: + # device serial device, e.g. /dev/ttyUSB0 + # stopbits number of stop bits + # parity parity setting, N, E or O + # baud baud rate + + timeout = config.getint("timeout", fallback=1) + retries = config.getint("retries", fallback=3) + unit = config.getint("src_address", fallback=1) + + host = config.get("host", fallback=False) + port = config.getint("port", fallback=False) + device = config.get("device", fallback=False) + + if device: + stopbits = config.getint("stopbits", fallback=1) + parity = config.get("parity", fallback="N") + baud = config.getint("baud", fallback=2400) + + if (parity + and parity.upper() in ["N", "E", "O"]): + parity = parity.upper() + else: + parity = False + + return solaredge_modbus.Inverter( + device=device, + stopbits=stopbits, + parity=parity, + baud=baud, + timeout=timeout, + retries=retries, + unit=unit + ) + else: + return solaredge_modbus.Inverter( + host=host, + port=port, + timeout=timeout, + retries=retries, + unit=unit + ) + + +def values(device): + if not device: + return {} + + logger = logging.getLogger() + logger.debug(f"device: {device}") + + values = {} + inverter_values=device.read_all() + + # append type to key to prevent key name collision with legacy values + values = {key+'_'+re.search('\'(.*)\'',str(type(value))).group(1):value for key, value in inverter_values.items()} + + meters = device.meters() + batteries = device.batteries() + values["connected_meters"] = {} + values["connected_batteries"] = {} + + for meter, params in meters.items(): + meter_values = params.read_all() + values["connected_meters"][meter] = {key+'_'+re.search('\'(.*)\'',str(type(value))).group(1):value for key, value in meter_values.items()} + + for battery, params in batteries.items(): + battery_values = params.read_all() + values["connected_batteries"][battery] = {key+'_'+re.search('\'(.*)\'',str(type(value))).group(1):value for key, value in battery_values.items()} + + logger.debug(f"values: {values}") + + # additional values for emulation of SE-WNC-3Y-400-MB-K1 or WattNode WNC-3Y-400-MB + + # TODO Calculate the values for the SE-WNC-3Y-400-MB-K1 meter from the SolarEdge meter provided by SE7K + + meterValues = values["connected_meters"]["Meter1"] + + + SE_WNC_3Y_400_MB_K1_values = { + "energy_active": meterValues.get('export_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "import_energy_active": meterValues.get('import_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "power_active": meterValues.get('power_int', 0)*10**meterValues.get('power_scale_int', 0), + "l1_power_active": meterValues.get('l1_power_int', 0)*10**meterValues.get('power_scale_int', 0), + "l2_power_active": meterValues.get('l2_power_int', 0)*10**meterValues.get('power_scale_int', 0), + "l3_power_active": meterValues.get('l3_power_int', 0)*10**meterValues.get('power_scale_int', 0), + "voltage_ln": meterValues.get('voltage_ln_int', 0)*10**meterValues.get('voltage_scale_int', 0), + "l1n_voltage": meterValues.get('l1n_voltage_int', 0)*10**meterValues.get('voltage_scale_int', 0), + "l2n_voltage": meterValues.get('l2n_voltage_int', 0)*10**meterValues.get('voltage_scale_int', 0), + "l3n_voltage": meterValues.get('l3n_voltage_int', 0)*10**meterValues.get('voltage_scale_int', 0), + "voltage_ll": meterValues.get('voltage_ll_int', 0)*10**meterValues.get('voltage_scale_int', 0), + "l12_voltage": meterValues.get('l12_voltage_int', 0)*10**meterValues.get('voltage_scale_int', 0), + "l23_voltage": meterValues.get('l23_voltage_int', 0)*10**meterValues.get('voltage_scale_int', 0), + "l31_voltage": meterValues.get('l31_voltage_int', 0)*10**meterValues.get('voltage_scale_int', 0), + "frequency": meterValues.get('frequency_int', 0)*10**meterValues.get('frequency_scale_int', 0), + "l1_energy_active": meterValues.get('l1_export_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "l2_energy_active": meterValues.get('l2_export_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "l3_energy_active": meterValues.get('l3_export_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "l1_import_energy_active": meterValues.get('l1_import_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "l2_import_energy_active": meterValues.get('l2_import_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "l3_import_energy_active": meterValues.get('l3_import_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "export_energy_active": meterValues.get('export_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "l1_export_energy_active": meterValues.get('l1_export_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "l2_export_energy_active": meterValues.get('l2_export_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "l3_export_energy_active": meterValues.get('l3_export_energy_active_int', 0)*10**meterValues.get('energy_active_scale_int', 0), + "energy_reactive": 0.0, + "l1_energy_reactive": 0.0, + "l2_energy_reactive": 0.0, + "l3_energy_reactive": 0.0, + "energy_apparent": meterValues.get('import_energy_apparent_int', 0)*10**meterValues.get('energy_apparent_scale_int', 0), + "l1_energy_apparent": meterValues.get('l1_import_energy_apparent_int', 0)*10**meterValues.get('energy_apparent_scale_int', 0), + "l2_energy_apparent": meterValues.get('l2_import_energy_apparent_int', 0)*10**meterValues.get('energy_apparent_scale_int', 0), + "l3_energy_apparent": meterValues.get('l3_import_energy_apparent_int', 0)*10**meterValues.get('energy_apparent_scale_int', 0), + "power_factor": meterValues.get('power_factor_int', 0)*10**meterValues.get('power_factor_scale_int', 0), + "l1_power_factor": meterValues.get('l1_power_factor_int', 0)*10**meterValues.get('power_factor_scale_int', 0), + "l2_power_factor": meterValues.get('l2_power_factor_int', 0)*10**meterValues.get('power_factor_scale_int', 0), + "l3_power_factor": meterValues.get('l3_power_factor_int', 0)*10**meterValues.get('power_factor_scale_int', 0), + "power_reactive": meterValues.get('power_reactive_int', 0)*10**meterValues.get('power_reactive_scale_int', 0), + "l1_power_reactive": meterValues.get('l1_power_reactive_int', 0)*10**meterValues.get('power_reactive_scale_int', 0), + "l2_power_reactive": meterValues.get('l2_power_reactive_int', 0)*10**meterValues.get('power_reactive_scale_int', 0), + "l3_power_reactive": meterValues.get('l3_power_reactive_int', 0)*10**meterValues.get('power_reactive_scale_int', 0), + "power_apparent": meterValues.get('power_apparent_int', 0)*10**meterValues.get('power_apparent_scale_int', 0), + "l1_power_apparent": meterValues.get('l1_power_apparent_int', 0)*10**meterValues.get('power_apparent_scale_int', 0), + "l2_power_apparent": meterValues.get('l2_power_apparent_int', 0)*10**meterValues.get('power_apparent_scale_int', 0), + "l3_power_apparent": meterValues.get('l3_power_apparent_int', 0)*10**meterValues.get('power_apparent_scale_int', 0), + "l1_current": meterValues.get('l1_current_int', 0)*10**meterValues.get('current_scale_int', 0), + "l2_current": meterValues.get('l2_current_int', 0)*10**meterValues.get('current_scale_int', 0), + "l3_current": meterValues.get('l3_current_int', 0)*10**meterValues.get('current_scale_int', 0), + "demand_power_active": 0.0, + "minimum_demand_power_active": 0.0, + "maximum_demand_power_active": 0.0, + "demand_power_apparent": 0.0, + "l1_demand_power_active": 0.0, + "l2_demand_power_active": 0.0, + "l3_demand_power_active": 0.0, + } + + + return dict(list(values.items()) + list(SE_WNC_3Y_400_MB_K1_values.items())) + # append type to key \ No newline at end of file diff --git a/documents/CarloGavazziEM24.pdf b/documents/CarloGavazziEM24.pdf new file mode 100644 index 0000000..db55240 Binary files /dev/null and b/documents/CarloGavazziEM24.pdf differ diff --git a/documents/Victron.txt b/documents/Victron.txt new file mode 100644 index 0000000..2162fad --- /dev/null +++ b/documents/Victron.txt @@ -0,0 +1,5 @@ +Victron erwartet folgenden Zähler + +EM24DINAV23XE1X 1648 (0x670) + +Er muss im Register 0B de wert 1648 zurückgeben \ No newline at end of file diff --git a/documents/em24_e1_cp.pdf b/documents/em24_e1_cp.pdf new file mode 100644 index 0000000..50a6a7d Binary files /dev/null and b/documents/em24_e1_cp.pdf differ diff --git a/init.d/modbus-proxy b/init.d/modbus-proxy new file mode 100644 index 0000000..d405845 --- /dev/null +++ b/init.d/modbus-proxy @@ -0,0 +1,87 @@ +#!/bin/sh +# Start/stop the modbus-proxy daemon. +# +### BEGIN INIT INFO +# Provides: cron +# Required-Start: $remote_fs $syslog $time +# Required-Stop: $remote_fs $syslog $time +# Should-Start: $network $named slapd autofs ypbind nscd nslcd winbind sssd +# Should-Stop: $network $named slapd autofs ypbind nscd nslcd winbind sssd +# Default-Start: 2 3 4 5 +# Default-Stop: +# Short-Description: Regular background program processing daemon +# Description: cron is a standard UNIX program that runs user-specified +# programs at periodic scheduled times. vixie cron adds a +# number of features to the basic UNIX cron, including better +# security and more powerful configuration options. +### END INIT INFO + +PATH=/bin:/usr/bin:/sbin:/usr/sbin +DESC="modbus proxy daemon" +NAME=modbus-proxy +DAEMON=/home/martin/gitHubClones/solaredge_meterproxy/SE7K-EM24-proxy-tcp.d.sh +PIDFILE=/var/run/modbus-proxy.pid +SCRIPTNAME=/etc/init.d/"$NAME" + +test -f $DAEMON || exit 0 + +. /lib/lsb/init-functions + +# +# We read /etc/environment, but warn about locale information in +# there because it should be in /etc/default/locale. +parse_environment () +{ + for ENV_FILE in /etc/environment /etc/default/locale; do + [ -r "$ENV_FILE" ] || continue + [ -s "$ENV_FILE" ] || continue + + for var in LANG LANGUAGE LC_ALL LC_CTYPE; do + value=`egrep "^${var}=" "$ENV_FILE" | tail -n1 | cut -d= -f2` + [ -n "$value" ] && eval export $var=$value + + if [ -n "$value" ] && [ "$ENV_FILE" = /etc/environment ]; then + log_warning_msg "/etc/environment has been deprecated for locale information; use /etc/default/locale for $var=$value instead" + fi + done + done + +# Get the timezone set. + if [ -z "$TZ" -a -e /etc/timezone ]; then + TZ=`cat /etc/timezone` + fi +} + +# Parse the system's environment +if [ "$READ_ENV" = "yes" ] ; then + parse_environment +fi + + +case "$1" in +start) log_daemon_msg "Starting modbus proxy service" "modbus_proxy" + start_daemon -p $PIDFILE $DAEMON $EXTRA_OPTS + log_end_msg $? + ;; +stop) log_daemon_msg "Stopping modbus proxy service" "modbus_proxy" + killproc -p $PIDFILE $DAEMON + RETVAL=$? + [ $RETVAL -eq 0 ] && [ -e "$PIDFILE" ] && rm -f $PIDFILE + log_end_msg $RETVAL + ;; +restart) log_daemon_msg "Restarting modbus proxy service" "modbus_proxy" + $0 stop + $0 start + ;; +reload|force-reload) log_daemon_msg "Reloading configuration files for modbus proxy service" "modbus_proxy" + $0 stop + $0 start + ;; +status) + status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $? + ;; +*) log_action_msg "Usage: /etc/init.d/modbus_proxy {start|stop|status|restart|reload|force-reload}" + exit 2 + ;; +esac +exit 0 diff --git a/init.d/modbus-proxy.md b/init.d/modbus-proxy.md new file mode 100644 index 0000000..1a98921 --- /dev/null +++ b/init.d/modbus-proxy.md @@ -0,0 +1,7 @@ +the modbus-proxy file must be stored in /etc/init.d and flagged executable. + +systemctl start modbus-proxy +systemctl enable modbus-proxy + +martin@raspberrypi:~ $ ps -ef |grep python +root 436 1 11 01:35 ? 00:00:44 python3 /home/martin/gitHubClones/solaredge_meterproxy/SE7K-proxy-tcp.py -c /home/martin/gitHubClones/solaredge_meterproxy/SE7K.conf diff --git a/raspberry-proxy.cmd b/raspberry-proxy.cmd new file mode 100644 index 0000000..76aa8ff --- /dev/null +++ b/raspberry-proxy.cmd @@ -0,0 +1,2 @@ +echo use -v for more information +python SE7K-proxy-tcp.py -c raspberry.conf %* \ No newline at end of file diff --git a/raspberry.conf b/raspberry.conf new file mode 100644 index 0000000..9923872 --- /dev/null +++ b/raspberry.conf @@ -0,0 +1,65 @@ +[server] +# Serving IP address. +# optional, default: all interfaces +address=0.0.0.0 +# ip-address=0.0.0.0 +# ip_address=0.0.0.0 + +# Serving port. +# optional, default: 5502 +port = 502 + +# Modbus frame type, set to rtu for Modbus RTU over TCP +# optional, default: socket +#framer = socket + +# Logging level, CRITICAL, ERROR, WARNING, INFO, DEBUG +# optional, default: INFO +log_level = INFO + +# Masqueraded meters, comma separated. +# optional, default: '' +meters = SE7K + +[SE7K] +type=solaredge-inverter +host=raspberrypi.fritz.box +port=502 +src_address=2 +dst_address=2 + + +# Meters defined in [server] need a config section, one per meter. +# Depending on the type of meter that is to be masqueraded, you can +# define a number of generic and type specific variables. + +# Modbus address of the meter as defined in the SolarEdge inverter. +# This value needs to be unique. +# optional, default: 2 +#dst_address = 2 + +# Source meter type, which corresponds to a script in /devices. +# The generic.py device returns null values. +# optional, default: generic +#type = generic + +# Masqueraded serial number. +# Need not be correct, must be unique, must be an integer. +# optional, default: 987654 +#serial_number = 987654 + +# Current transformer amperage rating. +# optional, default: 5 +#ct_current = 50 + +# Current transformer direction inversion, set to 1 if required. +# optional, default: 0 +#ct_inverted = 0 + +# Offset between phases, set to 0, 90, 120 or 180. +# optional, default: 0 +#phase_offset = 120 + +# Number of seconds between value refreshes. +# optional, default: 5 +refresh_rate = 0.5