Example on how to access data of a Sunny Boy SMA solar inverter

Hi all,
I’d like to share how I integrated my Sunny Boy SMA solar inverter into openHAB - in case somebody likes to do similar. In combination with the astro binding (sun rise, sun set) it can look like this:


You can ask your self if you start your dish washer now or maybe rather time it for tomorrow morning. :wink: Or even start to automate this. Have fun if you like it.

Basically my solution consists out of three things:

  • a python script to communicate with the inverter which delivers some data in a json format. It builds on the code by hdo in his git repository. I only have experience with the Sonnyboy inverter from SMA - therefor I don’t have any solution for other inverter brands. However github is full of examples - maybe you’re lucky for your brand.
  • a entry in the thing file to run the python script regularly
  • a rule to transform the json result into items. The rest should be straight forward.

sma.py - the python script

#!/usr/bin/python
import struct
import sys
import time
import json
from struct import *
from twisted.web import server, resource
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from twisted.application.internet import MulticastServer

user_pw = '0000' # default is '0000'
code_login = 0xfffd040d
code_total_today = 0x54000201
code_spot_ac_power = 0x51000201
src_serial = 2130422522 # change this to your serial number
dst_serial = 4294967295 
comm_port = 9522
comm_dst = '192.168.1.12' # change this to your IP

def get_encoded_pw(password):
    # user=0x88, install=0xBB
    encpw=[0x88,0x88,0x88,0x88,0x88,0x88,0x88,0x88,0x88,0x88,0x88,0x88]
    for index in range(min(len(encpw), len(password))):
        encpw[index] = encpw[index] + ord(password[index])

    ret = ""
    for ch in encpw:
        ret = ret + hex(ch).replace('0x','')
    return ret

cmd_login =                '534d4100000402a000000001003a001060650ea0ffffffffffff00017800%s00010000000004800c04fdff07000000840300004c20cb5100000000%s00000000' % (struct.pack('<I', src_serial).encode('hex'), get_encoded_pw(user_pw))
cmd_logout = '534d4100000402a00000000100220010606508a0ffffffffffff00037800%s000300000000d7840e01fdffffffffff00000000' % (struct.pack('<I', src_serial).encode('hex'))

cmd_query_total_today =    '534d4100000402a00000000100260010606509a0ffffffffffff0000780036abfb7e000000000000f180000200540001260022ff260000000000'
cmd_query_spot_ac_power =  '534d4100000402a00000000100260010606509e0ffffffffffff0000780036abfb7e00000000000081f0000200510001260022ff260000000000'

sma_data = {}
query_list = []
rea = 0

class MulticastClientUDP(DatagramProtocol):

  def datagramReceived(self, datagram, address):
      global sma_data, data_available
      data = datagram.encode('hex')
      code = get_code(datagram)

      if code == code_login:
         send_command(cmd_query_total_today)

      if len(datagram) <= 58:
         print "data access failed"
         reactor.stop()
         exit
      if code == code_total_today:
         total = get_long_value_at(datagram, 62)
         today = get_long_value_at(datagram, 78)
         sma_data['total'] = total
         sma_data['today'] = today
         send_command(cmd_query_spot_ac_power)
      if code == code_spot_ac_power:
         value = get_long_value_at(datagram, 62)
         if value == 0x80000000:
            value = 0
         sma_data['spotacpower'] = value
         output_data = json.dumps(sma_data)
         print output_data
         reactor.stop()

def send_command(cmd):
   data = cmd.decode('hex')
   rea.write(data, (comm_dst, comm_port))

def get_code(data):
   c = unpack('I', data[42:46])
   return c[0]

def get_long_value_at(data, index):
   v = unpack('I', data[index:index+4])
   return v[0]

def callfunc(x):
    reactor.stop()

rea = reactor.listenUDP(0, MulticastClientUDP())
_DelayedCallObj = reactor.callLater(5, callfunc, "callfunc called after 4 sec")
send_command(cmd_login)
reactor.run()

You have to make this script executable (chmod +x sma.py). If the script works well you should receive a output like {“spotacpower”: 450, “total”: 9543056, “today”: 21078}. If not debugging can be tricky. The dfault password should be the 0000. Try to use SBFSpot from SMA to get more information from your SMA inverter.

The entry in the things file:
(change the path to your script)

Thing exec:command:sunnyboy [command="/home/me/sma.py", interval=301, timeout=5, autorun=true]

The rule

rule "Solaranlage"
  when
     Item SunnyBoy_Out received update
  then
     
    logInfo("Sonnyboy ", SunnyBoy_Out.state.toString.trim )
    var String data = SunnyBoy_Out.state.toString
    var String s_Total = transform("JSONPATH", "$.total", data)
    var String s_Today = transform("JSONPATH", "$.today", data)
    var String s_Spot = transform("JSONPATH", "$.spotacpower", data)
    
    Solar_Total.postUpdate(new BigDecimal(s_Total) / 1000)            
    Solar_Today.postUpdate( (Float::parseFloat(s_Today) as Number )  )
    Solar_Spot.postUpdate( (Float::parseFloat(s_Spot) as Number )  )
    Solar_CO2.postUpdate( (Float::parseFloat(s_Today) as Number )*0.00067  )
end

And finally entries in items…:

Number Solar_Spot  "Solarstrom jetzt [%.0f W]" <solar> (gDG,gMapDB) 
Number Solar_Today "Solarstrom heute [%.0f Wh]" <solar> (gDG,gMapDB) 
Number Solar_CO2 "CO2-Einsparung heute [%.1f kg]" <leaf> (gDG,gMapDB) 
Number Solar_Total "Solarstrom Total [%.0f kWh]" <solar> (gDG,gMapDB) 

…and sitemap:

Frame label="Solar"
        {
            Text item=Solar_Spot
            Text item=Solar_Today
            Text item=Solar_Total
            Text item=Solar_CO2
        }
11 Likes

Many thanks for this. Could I ask which model of inverter you have, and which interface? (I have a Sunny Boy but no obvious interface)

Dan

I have a Sunny Boy 4000TL-21 which has a SMA Speedwire/ Webconnect already included as standard. All it took was was to open up the hood and plug in a LAN cable.

Jan

thanks - sadly mine doesn’t…

maybe you can upgrade your model. A speedwire module costs around ~130 EUR.

1 Like

Thanks for the tutorial. Very clear and easy to follow.

My only suggestion is it would be a tad cleaner to use BigDecimal.

Solar_Total.postUpdate(new BigDecimal(s_Total) / 1000)
1 Like

Thanks, Rich. Makes sense. I updated the post accordingly.

I have a SMA Sunnyboy 2.5 (battery inverter) with LAN as well. Will it work the same?
If yes - The pw is it the Installer pw or the User pw ?

I recently obtained a Sunny Boy SB2.5-1VL40 battery inverter with integrated WLAN.

It would be great to monitor my solar panels through OpenHAB, so I was very happy to find this solution. Honestly, I’m not to familiar with Python scripts yet.
I saved the script on my Raspberry2 in the directory /etc/openhab2/scripts and made the script executable.

The inverter produces data in my network through its own webserver, but I don’t receive data in OpenHAB. How can I test that the script is functioning properly ?

Sorry, Kim. Wasn’t in the forum for a while. To be honest - I don’t know if it works with Sunnyboy 2.5 as I don’t have one, but there is a good chance that it does. The PW is an internal one, not the one you use for the SunnyPortal - default is 0000.

There is some more software around to test. Check out this web site for SBFSpot

Hi Pieter,
log into the machine where you have openHAB running via SSH terminal (like Putty) and run the script in the by going into that directory and type “./sma.py”.

You should see something like {“spotacpower”: 0, “total”: 10538387, “today”: 7320} if the script is working. If not it depends you error message.

One more thing: check if you have to changed the path to your script in the thing file (
command=/etc/openhab2/scripts instead of command="/home/me/sma.py" in your case).

Hi Jan,

I’m having problems accessing the inverter:

data access failed
Unhandled Error
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/twisted/python/log.py", line 86, in callWithContext
    return context.call({ILogContext: newCtx}, func, *args, **kw)
  File "/usr/lib/python2.7/dist-packages/twisted/python/context.py", line 118, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File "/usr/lib/python2.7/dist-packages/twisted/python/context.py", line 81, in callWithContext
    return func(*args,**kw)
  File "/usr/lib/python2.7/dist-packages/twisted/internet/posixbase.py", line 597, in _doReadOrWrite
    why = selectable.doRead()
--- <exception caught here> ---
  File "/usr/lib/python2.7/dist-packages/twisted/internet/udp.py", line 249, in doRead
    self.protocol.datagramReceived(data, addr)
  File "sma.py", line 58, in datagramReceived
    total = get_long_value_at(datagram, 62)
  File "sma.py", line 81, in get_long_value_at
    v = unpack('I', data[index:index+4])
struct.error: unpack requires a string argument of length 4

I think it’s because the PV system password (installer password) was changed from the default 1111 to something different and longer. The 0000 isn’t working either.

Any ideas?

Hi Toon,
I’m afraid I don’t have a good answer for you. You probably only have two options:

  • Try to reset the password to a 4 digit password. Check out this web site for SBFSpot - maybe you can use that.

  • Rewrite the python code to it accepts string arguments of length larger then 4 (I assume this is the current issue)

Sorry
Jan

Hi Toon,

The Installer pwd is different in my case, but I’m not sure if that’s the problem.
I will try this today in my configuration.

Groeten,
Pieter

Hi Toon,

Your solution didn’t work for me. I received no reply at all.
Does the SunnyBoy communicate through Webconnect, TCP or UDP ?

My inverter has only the webconnect enabled.

Kind regards,
Pieter

Hi Jan,

I’d appreciate if you could help rewriting the code to allow for longer passwords. That’s outside my reach…

Thank you,
Toon

Thank you for the tutorial, it works like a charm.
As I have a SunnyBoy2.5 I had to create a custom Python program to read the integrated webserver.
I’m not sure if all fields are the same (ID can differ per inverter).
I can post an example to test, if wanted

Hi Lennert,

I’m very interested in your script, since I have the same inverter.
Could you place your version of the script in this topic ?

Kind regards,
Pieter Bruinsma

Here you are, very basic but it works. Change IP and password accordingly :slight_smile:
The only thing I’m not sure about are the ID/references, these might be different for you.
I’ve found out by running the script in parts and using “print r.text”

#!/usr/bin/python
import json
import requests
Total=0
Today=0
url = ‘http://192.168.1.31/dyn/login.json
payload = (‘{“pass” : “YourPassHere”, “right” : “usr”}’)
headers = {‘Content-Type’: ‘application/json’, ‘Accept-Charset’: ‘UTF-8’}
r = requests.post(url, data=payload, headers=headers)
j = json.loads(r.text)
sid = j[‘result’][‘sid’]
url = ‘http://192.168.1.31/dyn/getValues.json?sid=’ + sid
payload = (‘{“destDev”:,“keys”:[“6400_00260100”,“6400_00262200”]}’)
headers = {‘Content-Type’: ‘application/json’, ‘Accept-Charset’: ‘UTF-8’}
r = requests.post(url, data=payload, headers=headers)
j = json.loads(r.text)
Total = j[‘result’][‘012F-730ACDF3’][‘6400_00260100’][‘1’][0][‘val’]
Today = j[‘result’][‘012F-730ACDF3’][‘6400_00262200’][‘1’][0][‘val’]
d = {}
d[“Total”] = Total
d[“Today”] = Today
print json.dumps(d, ensure_ascii=False)

Thanks Lennert,

I will try this script in short time. I assume that I can use the items as described earlier in this topic. Where in your script do I have to fill in my inverter-ID ?

Kind regards,
Pieter