Real-time readout APSystems in Domoticz
We have recently become proud owners of solar panels. Because energy consumption has already been tracked in domoticz and am not in favor of different portals/apps to keep track of everything, I also wanted to have the yield of the solar panels displayed in domoticz.
After the supplier had mounted all the panels on the roof and the system was running, I asked if the ECU-R could also be read via domoticz. Unfortunately, this turned out to be shielded. But there was an app (EMA App) and a web portal with which you could read the consumption. Both options resulted in a delay of approximately five minutes. There was also the ECUAPP. By clicking on the side of the router, it broadcasts a WiFi signal for an hour. By connecting your phone/tablet to this WiFi hotspot, you can read the real-time values with the ECUAPP.
on Tweakers.net they would have also found out and a number of smart minds have already been busy reading a large part of the data via a Python script. It should be noted that this only works via WiFi.
A number of Tweakers members use Home Assistant. So the plug-in needs to be (slightly) adjusted so that it can be used for Domoticz.
Install Plugin
In addition to the code below, you also have the plug-in which can be downloaded from Github.
To run the python scripts on the Raspberry Pi Python 3.x to be installed.
#!/usr/bin/env python3 from APSystemsECUR import APSystemsECUR import time import asyncio import urllib.request import urllib.parse import urllib from pprint import pprint ecu_ip = " " sleep = 300 url = ' :8080/json.htm?' semicolon = '\u003B' loop = asyncio.get_event_loop() ecu = APSystemsECUR(ecu_ip) while True: try: data = loop.run_until_complete(ecu.async_query_ecu()) print(data) lifetime_energy = str(data.get('lifetime_energy ')*1000) today_energy_kwh = str(data.get('today_energy')*1000) current_power = str(data.get('current_power')) print('current_power: ' + current_power) generated_energy = (current_power + semicolon + lifetime_energy ) print('output: ' + today_energy_kwh + ';' + current_power) #pwr = .format(today_energy_kwh, current_power) #print('PWR: ' + pwr) print('Today energy [kWh]: ' + today_energy_kwh) if (float (today_energy_kwh) >= 0 or float(current_power) >= 0): getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1606, 'svalue' : (generated_energy)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) print(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') getVars = {'type ' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx': 1610, 'svalue': data.get('timestamp')} webUrl = urllib.request.urlopen(url + urllib .parse.urlencode(getVars)) #print(url + urllib.parse.urlencode(getVars)) #inverter values inverters = data.get('inverters') #count number of inverters Inverter_qty = len(data.get('inverters')) print('Inverter_cnt: ' + str(Inverter_qty)) # loop through all inverters and get the data for i in range(Inverter_qty): Inverter = list(inverters.keys())[i] print('InverterId: ' + Inverter ) InverterOnline = data['inverters'][Inverter]['online'] print('Online: ' + str(InverterOnline)) InverterTemperature = data['inverters'][Inverter]['temperature'] print('Temperature: ' + str(InverterTemperature)) nVoltage = len(data['inverters'][Inverter]['voltage']) nPower = len(data['inverters'][Inverter]['power']) for x in range( nPower): voltage = data['inverters'][Inverter]['voltage'][x] power = data['inverters'][Inverter]['power'][x] print('Power inverter ' + str( i + 1) + ' panel ' + str(x + 1) + ': ' + str(power) + ' W') #upload values to Domoticz for inverter 1 if (i == 0) and (x == 0) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx': 1624, 'svalue': voltage} webUrl = urllib.request.urlopen(url + urllib. parse.urlencode(getVars)) getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1609, 'svalue' : InverterTemperature} webUrl = urllib.request. urlopen(url + urllib.parse.urlencode(getVars)) if (InverterOnline == True) and (voltage != 0) : getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx' : 1607, 'switchcmd': 'On', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) elif (InverterOnline == True) and (voltage == 0) : getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1607, 'switchcmd': 'Off', 'level': 0, 'passcode' : '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) else : getVars = {'type' : 'command', 'param' : 'switchlight', 'idx': 1607, 'switchcmd': 'Off', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) #upload values to Domoticz for inverter 2 if (i == 1) and (x == 0) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1614, 'svalue' : InverterTemperature} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) if (InverterOnline == True) and (voltage != 0) : getVars = { 'type' : 'command', 'param' : 'switchlight ', 'idx': 1611, 'switchcmd': 'On', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) elif ( InverterOnline == True) and (voltage == 0) : getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1611, 'switchcmd': 'Off', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) else : getVars = {'type' : 'command', 'param' : 'switchlight', ' idx': 1611, 'switchcmd': 'Off', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) #upload values to Domoticz for inverter 3 if (i == 2) and (x == 0) : getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1615, 'svalue ': InverterTemperature} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) if (InverterOnline == True) and (voltage != 0) : getVars = {'type' : 'command', ' param' : 'switchlight', 'idx': 1612, 'switchcmd': 'On', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode( getVars)) elif (InverterOnline == True) and (voltage == 0) : getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx' : 1612, 'switchcmd' : 'Off' , 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) else : getVars = {'type' : 'command', 'param' : 'switchlight', 'idx': 1612, 'switchcmd': 'Off', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) #upload values to Domoticz for inverter 4 if (i == 3) and (x == 0) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1616, 'svalue': InverterTemperature} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) if (InverterOnline == True) and (voltage != 0) : getVars = {'type' : 'command', 'param' : 'switchlight', 'idx': 1613, 'switchcmd': 'On', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib .parse.urlencode(getVars)) elif (InverterOnline == True) and (voltage == 0) : getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx' : 1613, 'switchcmd ': 'Off', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars)) else : getVars = {'type' : 'command' , 'param' : 'switchlight', 'idx': 1613, 'switchcmd': 'Off', 'level': 0, 'passcode': '' } webUrl = urllib.request.urlopen(url + urllib.parse. urlencode(getVars)) #upload power values to Domoticz for inverter 1 if (i == 0) and (x == 0) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx': 1608, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 0 ) and (x == 1) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1617, 'svalue' : (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 0) and (x == 2) : getVars = {'type' : 'command' , 'param' : 'udevice', 'nvalue' : 0, 'idx': 198, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon ) + '0') elif (i == 0) and (x == 3) : getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx': 199, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') #upload power values to Domoticz for inverter 2 if (i == 1) and (x == 0) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1618, 'svalue' : (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 1) and (x == 1) : getVars = {'type' : 'command ', 'param' : 'udevice', 'nvalue' : 0, 'idx': 1619, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + ( semicolon) + '0') elif (i == 1) and (x == 2) : getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 202, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 1) and (x == 3) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 203, 'svalue' : (power)} webUrl = urllib.request.urlopen( url + urllib.parse.urlencode(getVars) + (semicolon) + '0') #upload power values to Domoticz for inverter 3 if (i == 2) and (x == 0) : getVars = {'type' : ' command', 'param' : 'udevice', 'nvalue' : 0, 'idx': 1620, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 2) and (x == 1) : getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx ': 1621, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 2) and (x = = 2) : getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 206, 'svalue' : (power)} webUrl = urllib.request.urlopen (url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 2) and (x == 3) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx': 207, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0' ) #upload power values to Domoticz for inverter 4 if (i == 3) and (x == 0) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, ' idx': 1622, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 3) and (x == 1) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1623, 'svalue' : (power)} webUrl = urllib.request. urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') elif (i == 3) and (x == 2) : getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx': 210, 'svalue': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0 ') elif (i == 3) and (x == 3) : getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 211, 'svalue ': (power)} webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0') except Exception as err: print(f"[ERROR]", {err} ) #print(f"Sleeping for {sleep} sec") time.sleep(sleep)
- Place the plugin and above script in your Domoticz folder under scripts/python/ECU-R.
- Create a Dummy sensor in Domoticz and call it 'Virtual switches' or another clear name.
Add dummy sensors in Domoticz
- Create a virtual sensor of the type 'Elektra (Current + counter)'. Name the sensor 'Solar generation'.
- Create a virtual sensor of type 'Switch' and name it Inverter [number inverter].
- Repeat this step for the number of microinverters you have.
- Create a virtual sensor of the type 'Consumption (Electric)' and name it Inverter [number inverter] – Power [panel number].
- Repeat this step for the number of panels per microinverter.
- Create a virtual sensor of type 'Temperature' and name it Inverter [number inverter] – Temperature.
- Repeat this step for the number of microinverters you have.
- Create a virtual sensor of type 'Text' and name it Timestamp.
- Go to 'Devices' and adjust the idx's in the above script to the idx of the virtual sensors you just created and save the script.
- Start the script with the command
python3 /scripts/python/ECU-R/ECU-R.py
. - To have the script start automatically after every reboot of the Raspberry Pi, add the following line in crontab:
@reboot python3 /home/pi/domoticz/scripts/python/ECU-R/ECU-R.py
.
Update 25-01-2023
User Sebastiaan Terlouw has slightly modified the script so that it is now also compatible with the DS3 controller.
Update 2/15/2023
Sebastiaan Terlouw has added the mains voltage to the script.
Dit delen:
- Klik om te delen met Twitter (Wordt in een nieuw venster geopend)
- Klik om te delen op Facebook (Wordt in een nieuw venster geopend)
- Meer
- Klik om dit te e-mailen naar een vriend (Wordt in een nieuw venster geopend)
- Klik om op LinkedIn te delen (Wordt in een nieuw venster geopend)
- Klik om te delen met Reddit (Wordt in een nieuw venster geopend)
- Klik om op Tumblr te delen (Wordt in een nieuw venster geopend)
- Klik om te delen op Telegram (Wordt in een nieuw venster geopend)
- Klik om te delen op WhatsApp (Wordt in een nieuw venster geopend)
Hello,
When I execute the script, I have this error :
[ERROR] {TypeError(“object of type 'NoneType' has no len()”)}
Any idea of what's wrong?
Hi JulienG,
Probably the variable data contains no data. You can test this by adding the following code between
data = loop.run_until_complete(ecu.async_query_ecu()) and print(data)
This is row 23 and 24.
if data is not None:
# Run the rest of the code that depends on data
lifetime_energy = str(data.get('lifetime_energy')1000)
today_energy_kwh = str(data.get('today_energy')1000)
current_power = str(data.get('current_power'))
# and so on…
else:
print(“[ERROR] No data received from ECU”)
Hello,
I use the script but it displays only one inverter.
Did you make all the inverters in Domoticz? And does all the idx correspondent within your script?
Hi,
yes it's me, it matches my idx, the script doesn't display an error message, it just does the sleep.
Hi Bjorn,
I've been using the script for about half a year and it worked perfectly, thanks again!
But since 2 days I have an error that makes it no longer work, maybe someone has an idea?
Thanks in advance.
[ERROR] {APSystemsInvalidData(“Unable to convert binary to int location=184 data=b'4150533131 etc long string
Hi Paul,
It seems that the data (or part of the data) from the inverters cannot be converted. You can test this by starting the script manually (e.g. in a Windows environment) and checking how the values are coming in and which values are coming in correctly.
Everything works again Bjorn, thanks for the tip!
Hi Bjorn,
Thanks for sharing your information.
Unfortunately, I get stuck at the next step: “Go to 'Devices' and adjust the idxes in the above script to the idx of the virtual sensors you just created and save the script.”
The sensors that I have created in Domoticz have numbers from 43 to 67. Should I regard these as IDXs and adjust / fill them in with VI in the already saved ECU-R.py script? Where in the script?
Regards,
Anton
Hi Anton,
Each device has its own IDX (identification number).
The script, which starts with #!/usr/bin/env python3, should be saved in the same directory as the ECU-R.py script. You then use an editor (e.g. Nano or VI) to adjust the idxes in the script to the idxes that your devices have.
Greeting,
Bjorn
Hi Björn, today there was an update to Domoticz 2023.1
This version is provided with (admin) User and Password by default.
Since then the ECU-R.py script has stopped working (HTTPError 401: 'Unauthorized'
Do you know if it is possible to provide login details here?
It has worked flawlessly for a year to date.
Greetings Willem van Oosterhout
Hi William,
I don't have Domoticz running anymore (I'd have to install it again to test). But can't you indicate on the basis of a (local) IP address that you don't have to log in? See the Wiki for more info domoticz.
Unfortunately, not yet found.
Back to the previous version then.
I thank you for your response.
Idd possible to specify local network as trusted, Works again. Thank you!
Super. Good luck and have fun with the panels!
and modified your script a bit
i saw that with me he has 4 controllers x 2 panels.
that he converted a Switch 16x while that was also sufficient with 4x:
The controller is also set to 'off' in domoticz if the Temp is 0 (because I see that happening when the controller switches off)
#!/usr/bin/env python3
from APSystemsECUR import APSystemsECUR
import time
import asyncio
import urllib.request
import urllib.parse
import urllib
from pprint import pprint
ecu_ip = "192.168.0.80"
tow = 300
url = 'http://192.168.0.200:8080/json.htm?'
semicolon = '\u003B'
loop = asyncio.get_event_loop()
ecu = APSystemsECUR(ecu_ip)
whileTrue:
try:
data = loop.run_until_complete(ecu.async_query_ecu())
#pprint(data)
lifetime_energy = str(data.get('lifetime_energy')*1000)
today_energy_kwh = str(data.get('today_energy')*1000)
current_power = str(data.get('current_power'))
print('current_power: ' + current_power)
generated_energy = (current_power + semicolon + lifetime_energy)
print('output: ' + today_energy_kwh + ';' + current_power)
#pwr = .format(today_energy_kwh, current_power)
#print('PWR: ' + pwr)
print('Today energy [kWh]: ' + today_energy_kwh)
if (float(today_energy_kwh) >= 0 or float(current_power) >= 0):
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1606, 'svalue' : (generated_energy)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
print(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
getVars = {'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx': 1610, 'svalue': data.get('timestamp')}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
#print(url + urllib.parse.urlencode(getVars))
#inverter values
inverters = data.get('inverters')
#count number of inverters
Inverter_qty = len(data.get('inverters'))
print('Inverter_cnt: ' + str(Inverter_qty))
# loop through all inverters and get the data
for i in range(Inverter_qty):
Inverter = list(inverters.keys())[i]
print('InverterId: ' + Inverter)
InverterOnline = data['inverters'][Inverter]['online']
print('Online: ' + str(InverterOnline))
InverterTemperature = data['inverters'][Inverter]['temperature']
print('Temperature: ' + str(InverterTemperature))
nPower = len(data['inverters'][Inverter]['power'])
for x in range(nPower):
power = data['inverters'][Inverter]['power'][x]
print('Power inverter ' + str(i + 1) + ' panel ' + str(x + 1) + ': ' + str(power) + ' W')
#upload values to Domoticz for inverter 1
if (i == 0) and (x == 0) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1609, 'svalue' : InverterTemperature}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
if InverterOnline == True :
getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1607, 'switchcmd': 'On', 'level': 0, 'passcode': '' }
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
else :
getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1607, 'switchcmd': 'Off', 'level': 0, 'passcode': '' }
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
#upload values to Domoticz for inverter 2
if (i == 1) and (x == 0) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1614, 'svalue' : InverterTemperature}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
if InverterOnline == True :
getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1611, 'switchcmd': 'On', 'level': 0, 'passcode': '' }
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
else :
getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1611, 'switchcmd': 'Off', 'level': 0, 'passcode': '' }
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
#upload values to Domoticz for inverter 3
if (i == 2) and (x == 0) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1615, 'svalue' : InverterTemperature}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
if InverterOnline == True :
getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1612, 'switchcmd': 'On', 'level': 0, 'passcode': '' }
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
else :
getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1612, 'switchcmd': 'Off', 'level': 0, 'passcode': '' }
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
#upload values to Domoticz for inverter 4
if (i == 3) and (x == 0) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1616, 'svalue' : InverterTemperature}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
if InverterOnline == True :
getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1613, 'switchcmd': 'On', 'level': 0, 'passcode': '' }
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
else :
getVars = { 'type' : 'command', 'param' : 'switchlight', 'idx': 1613, 'switchcmd': 'Off', 'level': 0, 'passcode': '' }
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars))
#upload power values to Domoticz for inverter 1
if (i == 0) and (x == 0) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1608, 'svalue' : (power)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
elif (i == 0) and (x == 1) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1617, 'svalue' : (power)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#elif (i == 0) and (x == 2) :
#getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 198, 'svalue' : (power)}
#webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#elif (i == 0) and (x == 3) :
#getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 199, 'svalue' : (power)}
#webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#upload power values to Domoticz for inverter 2
if (i == 1) and (x == 0) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1618, 'svalue' : (power)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
elif (i == 1) and (x == 1) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1619, 'svalue' : (power)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#elif (i == 1) and (x == 2) :
#getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 202, 'svalue' : (power)}
#webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#elif (i == 1) and (x == 3) :
#getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 203, 'svalue' : (power)}
#webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#upload power values to Domoticz for inverter 3
if (i == 2) and (x == 0) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1620, 'svalue' : (power)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
elif (i == 2) and (x == 1) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1621, 'svalue' : (power)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#elif (i == 2) and (x == 2) :
#getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 206, 'svalue' : (power)}
#webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#elif (i == 2) and (x == 3) :
#getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 207, 'svalue' : (power)}
#webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#upload power values to Domoticz for inverter 4
if (i == 3) and (x == 0) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1622, 'svalue' : (power)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
elif (i == 3) and (x == 1) :
getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 1623, 'svalue' : (power)}
webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#elif (i == 3) and (x == 2) :
#getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 210, 'svalue' : (power)}
#webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
#elif (i == 3) and (x == 3) :
#getVars = { 'type' : 'command', 'param' : 'udevice', 'nvalue' : 0, 'idx' : 211, 'svalue' : (power)}
#webUrl = urllib.request.urlopen(url + urllib.parse.urlencode(getVars) + (semicolon) + '0')
Exception as err:
print(f"[ERROR]", {err})
#print(f"Sleeping for {sleep} sec")
time.sleep(sleep)
APSystemsECUR adapted to be compatible with the DS3 controller:
#!/usr/bin/env python3
import asyncio
import socket
import binascii
import datetime
import json
import logging
_LOGGER = logging.getLogger(name)
from pprint import pprint
class APSystemsInvalidData(Exception):
pass
class APSystemsECUR:
def __init__(self, ipaddr, port=8899, raw_ecu=None, raw_inverter=None):
self.ipaddr = ipaddr
self.port = port
# what do we expect socket data to end in
self.recv_suffix = b'END\n'
# how long to wait on socket commands until we get our recv_suffix
self.timeout = 5
# how many times do we try the same command in a single update before failing
self.cmd_attempts = 3
# how big of a buffer to read at a time from the socket
self.recv_size = 4096
self.qs1_ids = [ "802", "801" ]
self.yc600_ids = [ "406", "407", "408", "409" ]
self.yc1000_ids = [ "501", "502", "503", "504" ]
self.ds3_ids = [ "703", "704" ]
self.cmd_suffix = "END\n"
self.ecu_query = "APS1100160001" + self.cmd_suffix
self.inverter_query_prefix = "APS1100280002"
self.inverter_query_suffix = self.cmd_suffix
self.inverter_signal_prefix = "APS1100280030"
self.inverter_signal_suffix = self.cmd_suffix
self.inverter_byte_start = 26
self.ecu_id = None
self.qty_of_inverters = 0
self.lifetime_energy = 0
self.current_power = 0
self.today_energy = 0
self.inverters = []
self.firmware = None
self.timezone = None
self.last_update = None
self.ecu_raw_data = raw_ecu
self.inverter_raw_data = raw_inverter
self.inverter_raw_signal = None
self.read_buffer = b''
self.reader = None
self.writer = None
def dump(self):
print(f"ECU : {self.ecu_id}")
print(f"Firmware : {self.firmware}")
print(f"TZ : {self.timezone}")
print(f"Qty of inverters : {self.qty_of_inverters}")
async def async_read_from_socket(self):
self.read_buffer = b''
end_data = None
while end_data != self.recv_suffix:
self.read_buffer += await self.reader.read(self.recv_size)
size = len(self.read_buffer)
end_data = self.read_buffer[size-4:]
return self.read_buffer
async def async_send_read_from_socket(self, cmd):
current_attempt = 0
while current_attempt < self.cmd_attempts:
current_attempt += 1
self.writer.write(cmd.encode('utf-8'))
await self.writer.drain()
try:
return await asyncio.wait_for(self.async_read_from_socket(),
timeout=self.timeout)
Exception as err:
pass
self.writer.close()
raise APSystemsInvalidData(f"Incomplete data from ECU after {current_attempt} attempts, cmd='{cmd.rstrip()}' data={self.read_buffer}")
async def async_query_ecu(self):
self.reader, self.writer = await asyncio.open_connection(self.ipaddr, self.port)
_LOGGER.info(f"Connected to {self.ipaddr} {self.port}")
cmd = self.ecu_query
self.ecu_raw_data = await self.async_send_read_from_socket(cmd)
self.process_ecu_data()
cmd = self.inverter_query_prefix + self.ecu_id + self.inverter_query_suffix
self.inverter_raw_data = await self.async_send_read_from_socket(cmd)
cmd = self.inverter_signal_prefix + self.ecu_id + self.inverter_signal_suffix
self.inverter_raw_signal = await self.async_send_read_from_socket(cmd)
self.writer.close()
data = self.process_inverter_data()
data["ecu_id"] = self.ecu_id
data["today_energy"] = self.today_energy
data["lifetime_energy"] = self.lifetime_energy
data["current_power"] = self.current_power
return(data)
def query_ecu(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.ipaddr,self.port))
sock.sendall(self.ecu_query.encode('utf-8'))
self.ecu_raw_data = sock.recv(self.recv_size)
self.process_ecu_data()
cmd = self.inverter_query_prefix + self.ecu_id + self.inverter_query_suffix
sock.sendall(cmd.encode('utf-8'))
self.inverter_raw_data = sock.recv(self.recv_size)
cmd = self.inverter_signal_prefix + self.ecu_id + self.inverter_signal_suffix
sock.sendall(cmd.encode('utf-8'))
self.inverter_raw_signal = sock.recv(self.recv_size)
socket.shutdown(socket.SHUT_RDWR)
sock.close()
data = self.process_inverter_data()
data["ecu_id"] = self.ecu_id
data["today_energy"] = self.today_energy
data["lifetime_energy"] = self.lifetime_energy
data["current_power"] = self.current_power
return(data)
def aps_int(self, codec, start):
try:
return int(binascii.b2a_hex(codec[(start):(start+2)]), 16)
except ValueError as err:
debugdata = binascii.b2a_hex(codec)
raise APSystemsInvalidData(f"Unable to convert binary to int location={start} data={debugdata}")
def aps_short(self, codec, start):
try:
return int(binascii.b2a_hex(codec[(start):(start+1)]), 8)
except ValueError as err:
debugdata = binascii.b2a_hex(codec)
raise APSystemsInvalidData(f"Unable to convert binary to short int location={start} data={debugdata}")
def aps_double(self, codec, start):
try:
return int (binascii.b2a_hex(codec[(start):(start+4)]), 16)
except ValueError as err:
debugdata = binascii.b2a_hex(codec)
raise APSystemsInvalidData(f"Unable to convert binary to double location={start} data={debugdata}")
def aps_bool(self, codec, start):
return bool(binascii.b2a_hex(codec[(start):(start+2)]))
def aps_uid(self, codec, start):
return str(binascii.b2a_hex(codec[(start):(start+12)]))[2:14]
def aps_str(self, codec, start, amount):
return str(codec[start:(start+amount)])[2:(amount+2)]
def aps_timestamp(self, codec, start, amount):
timestr=str(binascii.b2a_hex(codec[start:(start+amount)]))[2:(amount+2)]
return timestr[0:4]+"-"+timestr[4:6]+"-"+timestr[6:8]+" "+timestr[8:10]+":"+timestr[10:12] +":"+timestr[12:14]
def check_ecu_checksum(self, data, cmd):
datalen = len(data) - 1
try:
checksum = int(data[5:9])
except ValueError as err:
debugdata = binascii.b2a_hex(data)
raise APSystemsInvalidData(f"Error getting checksum int from '{cmd}' data={debugdata}")
if data != checksum:
debugdata = binascii.b2a_hex(data)
raise APSystemsInvalidData(f"Checksum on '{cmd}' failed checksum={checksum} data={data} data={debugdata}")
start_str = self.aps_str(data, 0, 3)
end_str = self.aps_str(data, len(data) - 4, 3)
if start_str != 'APS':
debugdata = binascii.b2a_hex(data)
raise APSystemsInvalidData(f"Result on '{cmd}' incorrect start signature '{start_str}' != APS data={debugdata}")
if end_str != 'END':
debugdata = binascii.b2a_hex(data)
raise APSystemsInvalidData(f"Result on '{cmd}' incorrect end signature '{end_str}' != END data={debugdata}")
returnTrue
def process_ecu_data(self, data=None):
if not dates:
data = self.ecu_raw_data
self.check_ecu_checksum(data, "ECU Query")
self.ecu_id = self.aps_str(data, 13, 12)
self.qty_of_inverters = self.aps_int(data, 46)
self.firmware = self.aps_str(data, 55, 15)
self.timezone = self.aps_str(data, 70, 9)
self.lifetime_energy = self.aps_double(data, 27) / 10
self.today_energy = self.aps_double(data, 35) / 100
self.current_power = self.aps_double(data, 31)
def process_signal_data(self, data=None):
signal_data = {}
if not dates:
data = self.inverter_raw_signal
self.check_ecu_checksum(data, "Signal Query")
if not self.qty_of_inverters:
return signal_data
location = 15
for i in range(0, self.qty_of_inverters):
uid = self.aps_uid(data, location)
location += 6
strength = data[location]
location += 1
strength = int((strength / 255) * 100)
signal_data[uid] = strength
return signal_data
def process_inverter_data(self, data=None):
if not dates:
data = self.inverter_raw_data
self.check_ecu_checksum(data, "Inverter data")
output = {}
timestamp = self.aps_timestamp(data, 19, 14)
inverter_qty = self.aps_int(data, 17)
self.last_update = timestamp
output["timestamp"] = timestamp
output["inverter_qty"] = inverter_qty
output["inverters"] = {}
# this is the start of the loop of inverters
location = self.inverter_byte_start
signal = self.process_signal_data()
inverters = {}
for i in range(0, inverter_qty):
inv={}
inverter_uid = self.aps_uid(data, location)
inv["uid"] = inverter_uid
location += 6
inv["online"] = self.aps_bool(data, location)
location += 1
inv["unknown"] = self.aps_str(data, location, 2)
location += 2
inv["frequency"] = self.aps_int(data, location) / 10
location += 2
inv["temperature"] = self.aps_int(data, location) - 100
location += 2
inv["signal"] = signal.get(inverter_uid, 0)
# the first 3 digits determine the type of inverter
inverter_type = inverter_uid[0:3]
if inverter_type in self.yc600_ids:
(channel_data, location) = self.process_yc600(data, location)
inv.update(channel_data)
elif inverter_type in self.qs1_ids:
(channel_data, location) = self.process_qs1(data, location)
inv.update(channel_data)
elif inverter_type in self.yc1000_ids:
(channel_data, location) = self.process_yc1000(data, location)
inv.update(channel_data)
elif inverter_type in self.ds3_ids:
(channel_data, location) = self.process_ds3(data, location)
inv.update(channel_data)
else:
raise APSystemsInvalidData(f"Unsupported inverter type {inverter_type}")
inverters[inverter_uid] = inv
output["inverters"] = inverters
return (output)
def process_yc1000(self, data, location):
power = []
voltages = []
power.append(self.aps_int(data, location))
location += 2
voltage = self.aps_int(data, location)
location += 2
power.append(self.aps_int(data, location))
location += 2
voltage = self.aps_int(data, location)
location += 2
power.append(self.aps_int(data, location))
location += 2
voltage = self.aps_int(data, location)
location += 2
power.append(self.aps_int(data, location))
location += 2
voltages.append(voltage)
output = {
"model" : "YC1000",
"channel_qty" : 4,
"power" : power,
"voltage" : voltages
}
return (output, location)
def process_qs1(self, data, location):
power = []
voltages = []
power.append(self.aps_int(data, location))
location += 2
voltage = self.aps_int(data, location)
location += 2
power.append(self.aps_int(data, location))
location += 2
power.append(self.aps_int(data, location))
location += 2
power.append(self.aps_int(data, location))
location += 2
voltages.append(voltage)
output = {
"model" : "QS1",
"channel_qty" : 4,
"power" : power,
"voltage" : voltages
}
return (output, location)
def process_yc600(self, data, location):
power = []
voltages = []
for i in range(0, 2):
power.append(self.aps_int(data, location))
location += 2
voltages.append(self.aps_int(data, location))
location += 2
output = {
"model" : "YC600",
"channel_qty" : 2,
"power" : power,
"voltage" : voltages,
}
return (output, location)
def process_ds3(self, data, location):
power = []
voltages = []
for i in range(0, 2):
power.append(self.aps_int(data, location))
location += 2
voltages.append(self.aps_int(data, location))
location += 2
output = {
"model" : "ds3",
"channel_qty" : 2,
"power" : power,
"voltage" : voltages,
}
return (output, location)
Hi Bjorn,
I get the following error messages.
Do you know what that could be?
File “/home/pi/domoticz/scripts/python/ECU-R/ECU-R.py”, line 23, in
data = loop.run_until_complete(ecu.async_query_ecu())
File “/usr/lib/python3.7/asyncio/base_events.py”, line 571, in run_until_complete
self.run_forever()
File “/usr/lib/python3.7/asyncio/base_events.py”, line 539, in run_forever
self._run_once()
File “/usr/lib/python3.7/asyncio/base_events.py”, line 1775, in _run_once
handle._run()
File “/usr/lib/python3.7/asyncio/events.py”, line 88, in _run
self._context.run(self._callback, *self._args)
File “/home/pi/domoticz/scripts/python/ECU-R/APSystemsECUR.py”, line 80, in async_read_from_socket
self.read_buffer += await self.reader.read(self.recv_size)
File “/usr/lib/python3.7/asyncio/streams.py”, line 644, in read
del self._buffer[:n]
Thanks in advance.
Greetings Jaden
Hi Jaden,
I can't say what caused this. Could Google provide some clarity?
Greeting,
Bjorn.
Thanks for the work you've put into it.
I also got the error message that “today_energy_kwh” was not defined.
When I read the code carefully, I saw that on line 27 it says:
'today_energy_kWh = str(data.get('today_energy')*1000)'
The rest of the code uses “today_energy_kwh” (with a small 'w')
After adjusting rule 27 it works like a train.
Thanks again!
Hi Fred,
Thanks for sharing and your attention. Changed the script.
Greeting,
Bjorn
Hi Bjorn,
Should have read better, but isn't it true that the Raspberry must be connected to the AP of the ECU-R? Retrieving the data via the local assigned IP address of my router, certainly does not work unfortunately!
Greeting,
Albert
Hi Albert,
Is the ECU set to WiFi? It will not work if set up via Ethernet cable.
Hi Bjorn,
Could you also indicate how you install the plugin, I use Domoticz and no Home Assistant.
Greeting,
Albert
Hi Albert,
It is not a plugin, but a script that runs in the background. The script sends the data via JSON to Domoticz.
An APSystemsECUR plug-in only needs to be downloaded from the Github page. I saw that the plugin has been updated and changed its name. I have now made the file available through my site.
The file APSystemsECUR.py should be saved in the same folder as the script above.
If you have any further questions, please let me know.
Sorry for the late reply, I already got your script working. Found on:
https://gathering.tweakers.net/forum/list_messages/2032302/3
From happy to see everything working until this afternoon, when it stopped, I don't know what the problem is yet, but I'll look again. Thank you for the response and the energy you put into it.
Ms. Willem
Hi William,
There is a button on the side of the ECU, usually when you press it, it works again.
Hi Bjorn, I'm getting the same error message as Michael:
[ERROR] {NameError("name 'today_energy_kwh' is not defined")}
current_power: 202
Could you perhaps indicate where this variable should be defined?
Python 3.7 is running on the Pi for me.
Thank you in advance,
Willem
Hi William,
I saw that the variable was not in the script. I have now added it.
str(data.get('lifetime_energy')*1000)
Let us know if it worked.
Greeting,
Bjorn
Hi Bjorn, I am getting the same error message as Michael.
[ERROR] {NameError("name 'today_energy_kwh' is not defined")}
Can you also indicate where this variable should be defined?
Thanks in advance for your help.
Yours sincerely,
Willem
Hi Bjorn,
I can't quite figure out from your text whether this will work, or whether the script still needs to be adjusted?
gr
Hi Sietse,
In principle, the script should work if you have 16 panels and 4 different inverters. If this is different in your case, the script needs to be adjusted slightly. Can you indicate what the situation is in your case?
Hi Bjorn,
Thank you so much for your script!!
I think I have taken all the steps as in your example, only I still get an error when executing.
Do you have any idea where I should look for it?
For example, does today_energy_kwh not exist in the APS ECU-R application? or have I created the virtual switch incorrectly in Domoticz?
pi@Domo:~/domoticz/scripts/python/ECU-R $ python3 ECU-R.py
current_power: 354
[ERROR] {NameError("name 'today_energy_kwh' is not defined")}
current_power: 354
[ERROR] {NameError("name 'today_energy_kwh' is not defined")}
current_power: 354
[ERROR] {NameError("name 'today_energy_kwh' is not defined")}
current_power: 354
[ERROR] {NameError("name 'today_energy_kwh' is not defined")}
Thanks in advance for your help.
gr
Michael
Hi Michael,
Are you sure you are using python 3?
Another option might be that the variable 'today_energy_kwh' is not (correctly) defined.