#!/usr/bin/env python """ SDM120c Energy Meter Author: Xavier Beaudouin Requirements: 1. modbus over TCP adapter like PW21 2. pymodbus AND pymodbusTCP """ """ """ import Domoticz import sys sys.path.append('/usr/local/lib/python3.4/dist-packages') sys.path.append('/usr/local/lib/python3.5/dist-packages') sys.path.append('/usr/local/lib/python3.6/dist-packages') sys.path.append('/usr/local/lib/python3.7/dist-packages') sys.path.append('/usr/local/lib/python3.8/dist-packages') sys.path.append('/usr/local/lib/python3.9/dist-packages') sys.path.append('/usr/local/lib/python3.10/dist-packages') import pymodbus from pyModbusTCP.client import ModbusClient from pymodbus.constants import Endian from pymodbus.payload import BinaryPayloadDecoder # # Domoticz shows graphs with intervals of 5 minutes. # When collecting information from the inverter more frequently than that, then it makes no sense to only show the last value. # # The Average class can be used to calculate the average value based on a sliding window of samples. # The number of samples stored depends on the interval used to collect the value from the inverter itself. # class Average: def __init__(self): self.samples = [] self.max_samples = 30 def set_max_samples(self, max): self.max_samples = max if self.max_samples < 1: self.max_samples = 1 def update(self, new_value, scale = 0): self.samples.append(new_value * (10 ** scale)) while (len(self.samples) > self.max_samples): del self.samples[0] Domoticz.Debug("Average: {} - {} values".format(self.get(), len(self.samples))) def get(self): return sum(self.samples) / len(self.samples) def strget(self): return str(sum(self.samples) / len(self.samples)) # Plugin itself class BasePlugin: def __init__(self): # Voltage for last 5 minutes self.voltage=Average() # Current for last 5 minutes self.current=Average() # Apparent Power for last 5 minutes self.apparent_power=Average() # Active power for last 5 minutes self.active_power=Average() # Reactive power for last 5 minutes self.reactive_power=Average() # Power factor for last 5 minutes self.power_factor=Average() # Phase Angle for last 5 minutes self.phase_angle=Average() # Frequency for last 5 minutes self.frequency=Average() # Total demand power for last 5 minutes self.total_demand_power=Average() # Import demand power for last 5 minutes self.import_demand_power=Average() # Export demand power for last 5 minutes self.export_demand_power=Average() # Total Demand Current for last 5 minutes self.total_demand_current=Average() return def onStart(self): Domoticz.Log("SDM120c Energy Meter TCP loaded!") # Check dependancies try: if (float(Parameters["DomoticzVersion"][:6]) < float("2020.2")): Domoticz.Error("WARNING: Domoticz version is outdated or not supported. Please update!") if (float(sys.version[:1]) < 3): Domoticz.Error("WARNING: Python3 should be used !") except: Domoticz.Error("Warning ! Dependancies could not be checked !") # Parse parameters # Debug if Parameters["Mode6"] == "Debug": Domoticz.Debugging(1) else: Domoticz.Debugging(0) self.IPAddress = Parameters["Address"] self.IPPort = Parameters["Port"] self.MBAddr = int(Parameters["Mode3"]) Domoticz.Debug("Query IP " + self.IPAddress + ":" + str(self.IPPort) +" on device : "+str(self.MBAddr)) # Create the devices if they does not exists if 1 not in Devices: Domoticz.Device(Name="Voltage", Unit=1, TypeName="Voltage", Used=0).Create() if 2 not in Devices: Domoticz.Device(Name="Current", Unit=2, TypeName="Current (Single)", Used=0).Create() if 3 not in Devices: Options = { "Custom": "1;W" } Domoticz.Device(Name="Active Power", Unit=3, TypeName="Custom", Used=0, Options=Options).Create() if 4 not in Devices: Options = { "Custom": "1;VA" } Domoticz.Device(Name="Apparent Power", Unit=4, TypeName="Custom", Used=0, Options=Options).Create() if 5 not in Devices: Options = { "Custom": "1;VAr" } Domoticz.Device(Name="Reactive Power", Unit=5, TypeName="Custom", Used=0, Options=Options).Create() if 6 not in Devices: Options = { "Custom": "1;PF" } Domoticz.Device(Name="Power Factor", Unit=6, TypeName="Custom", Used=0, Options=Options).Create() if 7 not in Devices: Options = { "Custom": "1;Deg" } Domoticz.Device(Name="Phase Angle", Unit=7, TypeName="Custom", Used=0, Options=Options).Create() if 8 not in Devices: Options = { "Custom": "1;Hz" } Domoticz.Device(Name="Frequency", Unit=8, TypeName="Custom", Used=0, Options=Options).Create() if 9 not in Devices: Domoticz.Device(Name="Import Energy", Unit=9, Type=243, Subtype=29, Used=0).Create() if 10 not in Devices: Domoticz.Device(Name="Export Energy", Unit=10, Type=243, Subtype=29, Used=0).Create() # 11 will be not used Import Energy (Reactive) / kVArh # 12 will be not used Export Energy (Reactive) / kVArh # TODO: collect this only if it is really wanted if 13 not in Devices: Options = { "Custom": "1;W" } Domoticz.Device(Name="Total Demand Power", Unit=13, TypeName="Custom", Used=0, Options=Options).Create() if 14 not in Devices: Options = { "Custom": "1;W" } Domoticz.Device(Name="Maximum Total Demand Power", Unit=14, TypeName="Custom", Used=0, Options=Options).Create() if 15 not in Devices: Options = { "Custom": "1;W" } Domoticz.Device(Name="Import Demand Power", Unit=15, TypeName="Custom", Used=0, Options=Options).Create() if 16 not in Devices: Options = { "Custom": "1;W" } Domoticz.Device(Name="Maximum Import Demand Power", Unit=16, TypeName="Custom", Used=0, Options=Options).Create() if 17 not in Devices: Options = { "Custom": "1;W" } Domoticz.Device(Name="Export Demand Power", Unit=17, TypeName="Custom", Used=0, Options=Options).Create() if 18 not in Devices: Options = { "Custom": "1;W" } Domoticz.Device(Name="Maximum Export Demand Power", Unit=18, TypeName="Custom", Used=0, Options=Options).Create() if 19 not in Devices: Domoticz.Device(Name="Total Demand Current", Unit=19, TypeName="Current (Single)", Used=0).Create() if 20 not in Devices: Domoticz.Device(Name="Maximum Total Demand Current", Unit=20, TypeName="Current (Single)", Used=0).Create() # End of TODO if 21 not in Devices: Domoticz.Device(Name="Total Energy (Active)", Unit=21, Type=0xfa, Subtype=0x01, Used=0).Create() # 22 will not be used Total Energy (Reactive) return def onStop(self): Domoticz.Debugging(0) def onHeartbeat(self): Domoticz.Debug(" Interface : IP="+self.IPAddress +", Port="+str(self.IPPort)+" ID="+str(self.MBAddr)) try: client = ModbusClient(host=self.IPAddress, port=self.IPPort, unit_id=self.MBAddr, auto_open=True, auto_close=True, timeout=2) except: Domoticz.Error("Error connecting to TCP/Interface on address : "+self.IPaddress+":"+str(self.IPPort)) # Set value to 0 -> Error on all devices Devices[1].Update(1, "0") Devices[2].Update(1, "0") Devices[3].Update(1, "0") Devices[4].Update(1, "0") Devices[5].Update(1, "0") Devices[6].Update(1, "0") Devices[7].Update(1, "0") Devices[8].Update(1, "0") Devices[9].Update(1, "0") Devices[10].Update(1, "0") Devices[13].Update(1, "0") Devices[14].Update(1, "0") Devices[15].Update(1, "0") Devices[16].Update(1, "0") Devices[17].Update(1, "0") Devices[18].Update(1, "0") Devices[19].Update(1, "0") Devices[20].Update(1, "0") Devices[21].Update(1, "0") #Domoticz.Log("Voltage : " + str(getmodbus(0x0000, client)) ) self.voltage.update(getmodbus(0x0000, client)) Devices[1].Update(1, self.voltage.strget()) #Domoticz.Log("Current : " + str(getmodbus(0x0006, client)) ) self.current.update(getmodbus(0x0006, client)) Devices[2].Update(1, self.current.strget()) #Domoticz.Log("Power Active : " + str(getmodbus(0x000c, client)) ) self.active_power.update(getmodbus(0x000c, client)) Devices[3].Update(1, self.active_power.strget()) #Domoticz.Log("Power apparent: " + str(getmodbus(0x0012, client)) ) self.apparent_power.update(getmodbus(0x0012, client)) Devices[4].Update(1, self.apparent_power.strget()) #Domoticz.Log("Power reactive: " + str(getmodbus(0x0018, client)) ) self.reactive_power.update(getmodbus(0x0018, client)) Devices[5].Update(1, self.reactive_power.strget()) #Domoticz.Log("Power Factor : " + str(getmodbus(0x001e, client)) ) self.power_factor.update(getmodbus(0x001e, client)) Devices[6].Update(1, self.power_factor.strget()) #Domoticz.Log("Phase Angle : " + str(getmodbus(0x0024, client)) ) self.phase_angle.update(getmodbus(0x0024, client)) Devices[7].Update(1, self.phase_angle.strget()) #Domoticz.Log("Frequency : " + str(getmodbus(0x0046, client)) ) self.frequency.update(getmodbus(0x0046, client)) Devices[8].Update(1, self.frequency.strget()) power = self.active_power.get() if power >= 0: import_power = power else: import_power = 0 if power < 0: export_power = abs(power) else: export_power = 0 #Domoticz.Log("Import NRJ act : " + str(getmodbus(0x0048, client)) ) import_e = str(getmodbus(0x0048, client)*1000) Devices[9].Update(1, sValue=str(import_power)+";"+import_e) #Domoticz.Log("Export NRJ act : " + str(getmodbus(0x004a, client)) ) export_e = str(getmodbus(0x004a, client)*1000) Devices[10].Update(1, sValue=str(export_power)+";"+export_e) #Domoticz.Log("Total Demand Pwr : " + str(getmodbus(0x0054, client)) ) self.total_demand_power.update(getmodbus(0x0054, client)) Devices[13].Update(1, self.total_demand_power.strget()) #Domoticz.Log("Max Demand Pwr : " + str(getmodbus(0x0056, client)) ) Devices[14].Update(1, str(getmodbus(0x0056, client))) #Domoticz.Log("Input Demand Pwr : " + str(getmodbus(0x0058, client)) ) self.import_demand_power.update(getmodbus(0x0058, client)) Devices[15].Update(1, self.import_demand_power.strget()) #Domoticz.Log("Max Input Demand Pwr : " + str(getmodbus(0x005a, client)) ) Devices[16].Update(1, str(getmodbus(0x005a, client))) #Domoticz.Log("Export Demand Pwr : " + str(getmodbus(0x005c, client)) ) self.import_demand_power.update(getmodbus(0x0058, client)) Devices[17].Update(1, self.import_demand_power.strget()) #Domoticz.Log("Max Export Demand Pwr : " + str(getmodbus(0x005e, client)) ) Devices[18].Update(1, str(getmodbus(0x005e, client))) #Domoticz.Log("Total Demand Cur: " + str(getmodbus(0x0102, client)) ) self.total_demand_current.update(getmodbus(0x0102, client)) Devices[19].Update(1, self.total_demand_current.strget()) #Domoticz.Log("Max Total Demand Cur: " + str(getmodbus(0x0108, client)) ) Devices[20].Update(1, str(getmodbus(0x0108, client))) #Domoticz.Log("Total Energy Act: " + str(getmodbus(0x0156, client)) ) Devices[21].Update(1, sValue=import_e+";0;"+export_e+";0;"+str(import_power)+";"+str(export_power)) global _plugin _plugin = BasePlugin() def onStart(): global _plugin _plugin.onStart() def onStop(): global _plugin _plugin.onStop() def onHeartbeat(): global _plugin _plugin.onHeartbeat() # Generic helper functions def DumpConfigToLog(): for x in Parameters: if Parameters[x] != "": Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'") Domoticz.Debug("Device count: " + str(len(Devices))) for x in Devices: Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x])) Domoticz.Debug("Device ID: '" + str(Devices[x].ID) + "'") Domoticz.Debug("Device Name: '" + Devices[x].Name + "'") Domoticz.Debug("Device nValue: " + str(Devices[x].nValue)) Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'") Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel)) return # get Modbus float 32 bits values def getmodbus(register, client): value = 0 try: data = client.read_input_registers(register, 2) Domoticz.Debug("Data from register "+str(register)+": "+str(data)) decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big) value = round(decoder.decode_32bit_float(), 3) except: Domoticz.Error("Error getting data from "+str(register) + ", try 1") try: data = client.read_input_registers(register, 2) Domoticz.Debug("Data from register "+str(register)+": "+str(data)) decoder = BinaryPayloadDecoder.fromRegisters(data, byteorder=Endian.Big, wordorder=Endian.Big) value = round(decoder.decode_32bit_float(), 3) except: Domoticz.Error("Error getting data from "+str(register) + ", try 2") return value