diff --git a/devices/em24.py b/devices/em24.py new file mode 100644 index 0000000..cc312f5 --- /dev/null +++ b/devices/em24.py @@ -0,0 +1,129 @@ +import logging +import sys +import sdm_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 sdm_modbus.EM24( + device=device, + stopbits=stopbits, + parity=parity, + baud=baud, + timeout=timeout, + retries=retries, + unit=unit + ) + else: + return sdm_modbus.EM24( + 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 = device.read_all(scaling = True) + + logger.debug(f"values: {values}") + + return { + "energy_active": values.get("import_energy_active", 0) + values.get("export_energy_active", 0), + "power_active": values.get("power_active", 0), + "l1_power_active": values.get("l1_power_active", 0), + "l2_power_active": values.get("l2_power_active", 0), + "l3_power_active": values.get("l3_power_active", 0), + "voltage_ln": values.get("voltage_ln", 0), + "l1n_voltage": values.get("l1_voltage", 0), + "l2n_voltage": values.get("l2_voltage", 0), + "l3n_voltage": values.get("l3_voltage", 0), + "voltage_ll": values.get("voltage_ll", 0), + "l12_voltage": values.get("l12_voltage", 0), + "l23_voltage": values.get("l23_voltage", 0), + "l31_voltage": values.get("l31_voltage", 0), + "frequency": values.get("frequency", 0), + "l1_energy_active": values.get("l1_import_energy_active", 0) + values.get("export_energy_active", 0)/3, + "l2_energy_active": values.get("l2_import_energy_active", 0) + values.get("export_energy_active", 0)/3, + "l3_energy_active": values.get("l3_import_energy_active", 0) + values.get("export_energy_active", 0)/3, + "import_energy_active": values.get("import_energy_active", 0), + "l1_import_energy_active": values.get("l1_import_energy_active", 0), + "l2_import_energy_active": values.get("l2_import_energy_active", 0), + "l3_import_energy_active": values.get("l3_import_energy_active", 0), + "export_energy_active": values.get("export_energy_active", 0), + "l1_export_energy_active": values.get("export_energy_active", 0)/3, + "l2_export_energy_active": values.get("export_energy_active", 0)/3, + "l3_export_energy_active": values.get("export_energy_active", 0)/3, + "energy_reactive": values.get("import_energy_reactive", 0) + values.get("export_energy_reactive", 0), + #"l1_energy_reactive" + #"l2_energy_reactive" + #"l3_energy_reactive" + #"energy_apparent" + #"l1_energy_apparent" + #"l2_energy_apparent" + #"l3_energy_apparent" + "power_factor": values.get("total_pf", 0), + "l1_power_factor": values.get("l1_power_factor", 0), + "l2_power_factor": values.get("l2_power_factor", 0), + "l3_power_factor": values.get("l3_power_factor", 0), + "power_reactive": values.get("power_reactive", 0), + "l1_power_reactive": values.get("l1_power_reactive", 0), + "l2_power_reactive": values.get("l2_power_reactive", 0), + "l3_power_reactive": values.get("l3_power_reactive", 0), + "power_apparent": values.get("power_apparent", 0), + "l1_power_apparent": values.get("l1_power_apparent", 0), + "l2_power_apparent": values.get("l2_power_apparent", 0), + "l3_power_apparent": values.get("l3_power_apparent", 0), + "l1_current": values.get("l1_current", 0), + "l2_current": values.get("l2_current", 0), + "l3_current": values.get("l3_current", 0), + "demand_power_active": values.get("demand_power_active", 0), + # "minimum_demand_power_active" + "maximum_demand_power_active": values.get("maximum_demand_power_active", 0), + # "demand_power_apparent" + #"l1_demand_power_active" + #"l2_demand_power_active" + #"l3_demand_power_active" + } diff --git a/semp-rtu.py b/semp-rtu.py index 04cd82d..beb339a 100755 --- a/semp-rtu.py +++ b/semp-rtu.py @@ -7,19 +7,19 @@ import sys import threading import time +import traceback from pymodbus.server import StartSerialServer from pymodbus.constants import Endian from pymodbus.device import ModbusDeviceIdentification -from pymodbus.transaction import ModbusRtuFramer from pymodbus.datastore import ModbusSlaveContext from pymodbus.datastore import ModbusServerContext from pymodbus.payload import BinaryPayloadBuilder - +# Protocol for WattNode register list: https://ctlsys.com/wp-content/uploads/2016/10/WNC-Modbus-Register-List-V18.xls def t_update(ctx, stop, module, device, refresh): - this_t = threading.currentThread() + this_t = threading.current_thread() logger = logging.getLogger() while not stop.is_set(): @@ -30,7 +30,7 @@ def t_update(ctx, stop, module, device, refresh): logger.debug(f"{this_t.name}: no new values") continue - block_1001 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1001 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1001.add_32bit_float(values.get("energy_active", 0)) # total active energy block_1001.add_32bit_float(values.get("import_energy_active", 0)) # imported active energy block_1001.add_32bit_float(values.get("energy_active", 0)) # total active energy non-reset @@ -50,7 +50,7 @@ def t_update(ctx, stop, module, device, refresh): block_1001.add_32bit_float(values.get("frequency", 0)) # line frequency ctx.setValues(3, 1000, block_1001.to_registers()) - block_1101 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1101 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1101.add_32bit_float(values.get("l1_energy_active", 0)) # total active energy l1 block_1101.add_32bit_float(values.get("l2_energy_active", 0)) # total active energy l2 block_1101.add_32bit_float(values.get("l3_energy_active", 0)) # total active energy l3 @@ -95,6 +95,7 @@ def t_update(ctx, stop, module, device, refresh): ctx.setValues(3, 1100, block_1101.to_registers()) except Exception as e: logger.critical(f"{this_t.name}: {e}") + print(traceback.format_exc()) finally: time.sleep(refresh) @@ -157,7 +158,7 @@ def t_update(ctx, stop, module, device, refresh): slave_ctx = ModbusSlaveContext() - block_1601 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1601 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1601.add_32bit_int(0) # config passcode block_1601.add_16bit_int(confparser[meter].getint("ct_current", fallback=default_config["meters"]["ct_current"])) # ct rated current block_1601.add_16bit_int(confparser[meter].getint("ct_current", fallback=default_config["meters"]["ct_current"])) # ct rated current l1 @@ -182,7 +183,7 @@ def t_update(ctx, stop, module, device, refresh): block_1601.add_16bit_int(0) # io pin mode slave_ctx.setValues(3, 1600, block_1601.to_registers()) - block_1651 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1651 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1651.add_16bit_int(0) # apply config block_1651.add_16bit_int(address) # modbus address block_1651.add_16bit_int(4) # baud rate @@ -191,7 +192,7 @@ def t_update(ctx, stop, module, device, refresh): block_1651.add_16bit_int(5) # message delay slave_ctx.setValues(3, 1650, block_1651.to_registers()) - block_1701 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1701 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1701.add_32bit_int(confparser[meter].getint("serial_number", fallback=default_config["meters"]["serial_number"])) # serial number block_1701.add_32bit_int(0) # uptime (s) block_1701.add_32bit_int(0) # total uptime (s) @@ -223,7 +224,7 @@ def t_update(ctx, stop, module, device, refresh): update_t_stop, meter_module, meter_device, - confparser[meter].getint("refresh_rate", fallback=default_config["meters"]["refresh_rate"]) + confparser[meter].getfloat("refresh_rate", fallback=default_config["meters"]["refresh_rate"]) ) ) @@ -247,7 +248,7 @@ def t_update(ctx, stop, module, device, refresh): StartSerialServer( context=server_ctx, - framer=ModbusRtuFramer, + framer="rtu", identity=identity, port=confparser["server"].get("device", fallback=default_config["server"]["device"]), baudrate=confparser["server"].get("baud", fallback=default_config["server"]["baud"]), diff --git a/semp-tcp.py b/semp-tcp.py index a4b815f..c24f733 100755 --- a/semp-tcp.py +++ b/semp-tcp.py @@ -7,20 +7,21 @@ import sys import threading import time +import traceback from pymodbus.server 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 - +# Protocol for WattNode register list: +# https://ctlsys.com/wp-content/uploads/2016/10/WNC-Modbus-Register-List-V18.xls +# https://ctlsys.com/wp-content/uploads/2016/10/WNC-Modbus-Manual-V18c.pdf def t_update(ctx, stop, module, device, refresh): - this_t = threading.currentThread() + this_t = threading.current_thread() logger = logging.getLogger() while not stop.is_set(): @@ -30,8 +31,8 @@ def t_update(ctx, stop, module, device, refresh): if not values: logger.debug(f"{this_t.name}: no new values") continue - - block_1001 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + + block_1001 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1001.add_32bit_float(values.get("energy_active", 0)) # total active energy block_1001.add_32bit_float(values.get("import_energy_active", 0)) # imported active energy block_1001.add_32bit_float(values.get("energy_active", 0)) # total active energy non-reset @@ -51,7 +52,7 @@ def t_update(ctx, stop, module, device, refresh): block_1001.add_32bit_float(values.get("frequency", 0)) # line frequency ctx.setValues(3, 1000, block_1001.to_registers()) - block_1101 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1101 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1101.add_32bit_float(values.get("l1_energy_active", 0)) # total active energy l1 block_1101.add_32bit_float(values.get("l2_energy_active", 0)) # total active energy l2 block_1101.add_32bit_float(values.get("l3_energy_active", 0)) # total active energy l3 @@ -96,6 +97,7 @@ def t_update(ctx, stop, module, device, refresh): ctx.setValues(3, 1100, block_1101.to_registers()) except Exception as e: logger.critical(f"{this_t.name}: {e}") + print(traceback.format_exc()) finally: time.sleep(refresh) @@ -157,7 +159,7 @@ def t_update(ctx, stop, module, device, refresh): slave_ctx = ModbusSlaveContext() - block_1601 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1601 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1601.add_32bit_int(1234) # config passcode block_1601.add_16bit_int(confparser[meter].getint("ct_current", fallback=default_config["meters"]["ct_current"])) # ct rated current block_1601.add_16bit_int(confparser[meter].getint("ct_current", fallback=default_config["meters"]["ct_current"])) # ct rated current l1 @@ -182,7 +184,7 @@ def t_update(ctx, stop, module, device, refresh): block_1601.add_16bit_int(0) # io pin mode slave_ctx.setValues(3, 1600, block_1601.to_registers()) - block_1651 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1651 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1651.add_16bit_int(0) # apply config block_1651.add_16bit_int(address) # modbus address block_1651.add_16bit_int(4) # baud rate @@ -191,7 +193,7 @@ def t_update(ctx, stop, module, device, refresh): block_1651.add_16bit_int(5) # message delay slave_ctx.setValues(3, 1650, block_1651.to_registers()) - block_1701 = BinaryPayloadBuilder(byteorder=Endian.Big, wordorder=Endian.Little) + block_1701 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) block_1701.add_32bit_int(confparser[meter].getint("serial_number", fallback=default_config["meters"]["serial_number"])) # serial number block_1701.add_32bit_int(0) # uptime (s) block_1701.add_32bit_int(0) # total uptime (s) @@ -213,6 +215,12 @@ def t_update(ctx, stop, module, device, refresh): block_1701.add_16bit_int(0) # error status 7 block_1701.add_16bit_int(0) # error status 8 slave_ctx.setValues(3, 1700, block_1701.to_registers()) + + block_2128 = BinaryPayloadBuilder(byteorder=Endian.BIG, wordorder=Endian.LITTLE) + block_2128.add_16bit_int(0) # SolarEdge requests the value for the register 2127 + # If you don't supply it, it will keep asking + # if you supply it, it will only ask it once + slave_ctx.setValues(3, 2127, block_2128.to_registers()) update_t_stop = threading.Event() update_t = threading.Thread( @@ -223,7 +231,7 @@ def t_update(ctx, stop, module, device, refresh): update_t_stop, meter_module, meter_device, - confparser[meter].getint("refresh_rate", fallback=default_config["meters"]["refresh_rate"]) + confparser[meter].getfloat("refresh_rate", fallback=default_config["meters"]["refresh_rate"]) ) ) @@ -240,9 +248,9 @@ def t_update(ctx, stop, module, device, refresh): framer = False if config_framer == "socket": - framer = ModbusSocketFramer + framer = "socket" elif config_framer == "rtu": - framer = ModbusRtuFramer + framer = "rtu" identity = ModbusDeviceIdentification() server_ctx = ModbusServerContext(slaves=slaves, single=False)