SMA Energy Meter Binding yields unplausible values

Yes and no.
No: Emeter delivers averaged values. So in short time ranges (200 ms) you will not see big flucutations in power consumption.
Yes: If you poll every 10 s on two different instances there will be differences visible since only by accident both instances will catch the same packet. Changes in power consumption less than 10 s may be catched by one instance but not to by the other. E. g. a peak of 1 kW with duration of 3 sec may be shown by one, both or none of them. But …

Any change in power consumption lasting longer than 10 sec. must be visible in both instances,
e.g. switching on a consumer which draws 1 kW for 1 min must result in an increase of displayed power consumption in the graphs of both instances. The following two pictures illustrate that. They are captured from my two raspberries running OH 3.1 with the old emeter binding. Both read the emeter every 10 sec with a shift of 5 sec inbetween the two. The red graph is powerIn.

As you can see, there are only small differences between the two graphs. Sometimes a small spike here or a higher bump there but the overall shape is the same. This is completely different from the pictures in my previous post where one of the two raspberries used the updated emeter binding.

But I think I can provide some more evidence proving that something is going wrong.
I made some measurements with the following setup:

  • Home Manger is 20 sending out the packets.
  • On my WIn10 pc three instances receive the packets.
    – openHab 3.1 stable, polling emeter packets every 10s
    – a python script displaying powerIn together with a time stamp at max rate of 200 ms
    – Wireshark monitoring the traffic on my network at max rate

My asumption: The powerIn value from incoming packets should be visible after a reasonable delay in all three receivers.

First I startet the measurements with the old emeter binding.Three results are shown below:

timestamp powerIn subsequent package subsequent package
Phyton script 12:33:30.184 6,70 10,2 10
OH 3.1, old binding 12:33:30.393 6,69999
Wireshark 12:33:30.397 6,70 10,2 10
Phyton script 12:37:40.326 383,9 322,4 232,9
OH 3.1, old binding 12:37:40.527 383,89999
Wireshark 12:37:40.540 383,9 322,4 232,9
Phyton script 12:43:00.266 54,1 54,6 52
OH 3.1, old binding 12:43:00.465 54,09999
Wireshark 12:43:00.466 54,1 54,6 52

As you can see, all three receivers get the same value nearly at the same time.The subsequent values are shown to prove that I got the rigth position in time.

Now the same with updated emeter binding:

starting timestamp powerIn subsequent package subsequent package
Phyton script 12:10:50.519 0,00 2,7 2,3
OH 3.1, new binding 12:10:50.634 22,90
Wireshark 12:10:50.716 0,00 2,7 2,3
Phyton script 12:16:10.428 0,00 0 0
OH 3.1, new binding 12:16:10.629 10,50
Wireshark 12:16:10.640 0,00 0 0
Phyton script 12:26:00.489 309,30 213 132,9
OH 3.1, new binding 12:26:00.635 3,20
Wireshark 12:26:00.702 309,30 213 132,9
The readings from python script and wireshark are still in sync, but the readings from the emeter binding are way off!

My conclusion: The updated emeter binding delivers wrong results. May be I was on the wrong track in my previous post saying there is a delay. If there is no buffering there can’t be a delay. But then sth. else must have been changed in the update.

Now, when I think of this, it makes more sense. It is unlikely related to the delay, but it might be that update of meter or frame resulted in buffer skew. I gonna have a look, if frame reader index is same for old and updated version of binding. Maybe we are one byte under or over.

Looking at the code I think that there could be few reasons why values are off in some ways. First of all old binding does send joinGroup request each time when poll attempt is made. Updated binding keep multicast listener open until there is at least one handler willing to receive data.

I’ve updated 3.1 and 3.2.0-SNAPSHOT versions of binding, please download it using links I mentioned earlier and test: SMA Energy Meter Binding yields unplausible values - #6 by splatch.

Thanks a lot for updating the code.
It seems you have changed the serialnumbers back to decimals. After some tries I was succesfull with the following entry in the thing file:

 smaenergymeter:energymeter:0123456789 [pollingPeriod="36000", serialNumber="0123456789"]

I’m using openHAB 3.1 stable. Could you please provide an updated 3.1 version of the binding?
I was able to start the 3.2 binding on openHab 3.1 but I haven’t enough insights to judge if mixing the versions would be a good idea.

At the first glance the results are looking promising. The energy values do what they should do → moving forward and not backward :slight_smile:
I will continue testing and compare the results with those of wireshark and and the old binding.

I believe now you should observe same results in your python script and binding. The old binding version did join multicast socket with each poll cycle, hence it could suffer a slight delay in receiving data. Current testing version keeps listening to socket and collect all values every 250ms, it just performs update after poll period passes.

As you noted identifier is back to decimal, this way I believe it is consistent with labeling of meters. Sorry for mess with hex form.
Also there is very basic timeout tracking which should put meter thing offline if data is not received within two poll cycles. If you specify poll cycle at 30s and there is no data received within 60s then thing will be put offline.

@splatch
I have tested the lasted version of org.openhab.binding.smaenergymeter-3.1.0.jar on both my win10 pc and my test raspberry. On the rapsberry I’m using openHAB 3.1.1. The binding is configured by means of a things file.

smaenergymeter:energymeter:0123456789 [pollingPeriod="10", serialNumber="0123456789"]

In my rules I use

SMAHomeManager20PowerIn.sendCommand(REFRESH)

to update the emeter values every 10 s.

Results:
The binding delivers the energymeter data as expected. Comparing the graphs produced by the old and the new binding there are only, expected, minor differences. I couldn’t see any unexprected powerIn values any more. The delivered values were also in sync with the output of my python script.

Nevertheless there were some sightings I would like to share with you:

  1. Each update of the things file caused a warning in openhab.log
2021-12-15 16:07:12.340 [WARN ] [mon.registry.AbstractManagedProvider] - Could not update element with key smaenergymeter:energymeter:3007890152 in ManagedThingProvider, because it does not exists.
  1. Setting poll rate to 0 caused an error in openhab.log and the binding stopped updating events. After having set back the poll rate to a non zero value the binding resumed event updates
2021-12-16 17:10:00.616 [ERROR] [nal.common.AbstractInvocationHandler] - An error occurred while calling method 'ThingHandler.thingUpdated()' on 'org.openhab.binding.smaenergymeter.internal.handler.SMAEnergyMeterHandler@ce8b57': null

	java.lang.IllegalArgumentException: null

		at java.util.concurrent.ScheduledThreadPoolExecutor.scheduleWithFixedDelay(ScheduledThreadPoolExecutor.java:671) ~[?:?]

		at org.openhab.binding.smaenergymeter.internal.handler.SMAEnergyMeterHandler.initialize(SMAEnergyMeterHandler.java:111) ~[?:?]

		at org.openhab.core.thing.binding.BaseThingHandler.thingUpdated(BaseThingHandler.java:152) ~[?:?]

		at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]

		at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]

		at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]

		at java.lang.reflect.Method.invoke(Method.java:566) ~[?:?]

		at org.openhab.core.internal.common.AbstractInvocationHandler.invokeDirect(AbstractInvocationHandler.java:154) [bundleFile:?]

		at org.openhab.core.internal.common.Invocation.call(Invocation.java:52) [bundleFile:?]

		at java.util.concurrent.FutureTask.run(FutureTask.java:264) [?:?]

		at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) [?:?]

		at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) [?:?]

		at java.lang.Thread.run(Thread.java:829) [?:?]
  1. After startup openHAB metrics showed a doubled event count for the emeter items compared to the old binding. In 2 minutes the old binding produced 12 event counts (poll rate 10 s → 6 events/min) while the new binding produced 24 events at the same poll rate. Event log updates appeared with the poll rate only.
    Even more, with each update of the things file the event count increased by 12. The picture below shows the event counts after 4 thing file updates.
    1. update: 36 event counts
    1. update: 48 event counts
    1. update: 60 event counts
    1. update: 72 event counts

Any thoughts?

You definitely observed a resource leak. Old receiver is not shut when update is being made still running in the background.

Hi

Thanks a lot for this new version that supports more than 1 device!
I have tested a few days now and everything worked like a charm :slight_smile: But today I saw 2 times this:

Would it be possible to automatically try to reconnect after say 1 minute? Because now I have to disable and enable the thing to get it “online” again.

I believe it does listen still for data, status indicates only that comms was lost for more than 2*polling period (this will change soon to multiplier of 200 ms used normally by meter), yet I don’t think I made any recovery logic so once thing goes offline you need to pause it and activate back which will give you issue described above by Thomas due to resource leak.

EDIT: Obviously OH cuts off channel updates from offline things but that’s other story :slight_smile:

You are rigth. After startup the karaf console already shows two threads consuming cpu and usr time which explains the 24 events in 2 minutes:

openhab> shell:threads --list | grep "sma"
604   x smaenergymeter-receiver-239.12.255.254:9522                                                x RUNNABLE      x 628274   x 553510
625   x smaenergymeter-receiver-239.12.255.254:9522                                                x RUNNABLE      x 620204   x 547570
13582 x pipe-grep "sma"                                                                            x RUNNABLE      x 7        x 0

Interestingly, with the old binding I could not even find one thread:

openhab> shell:threads --list | grep "sma"
75162 x pipe-grep "sma"                                                                            x RUNNABLE      x 7        x 0

@splatch I love to see you are working on that.
May I ask if there will be an update for 2.5 binding too?
I am still on 2.5 because it is working and I will not switch in near future.

Thanks for your help!

I’m busy with other things now, I had to park sma stuff. Not sure when I will find slot for it. There is definitely still some work to stabilize the reloading behavior and metadata handling. If someone with a bit of Java skill is still around please feel free to pick up stuff from gitbub branch I made.

Best,
Łukasz

1 Like

Since I’m lacking Java skills I helped myself with a python script. Others may find it useful too, so I would like to share it with the community.

  • The script returns all data found in the multicast packet issued by the SMA Homenager 2.0 with FW revision >= 2.03.4.R.
  • The data are decoded, adjusted and returned in a JSON string.
  • Packets with wrong protocol id are filtered.
  • A C type structure allows adaption to future protocol modifications.
  • The script can be used standalone for testing or in a DSL rule.

Python Code:

# -*- coding: utf-8 -*-.
"""SMA Home Manager 2.0, multicast interpreter.

MulticastIP 239.12.255.254, port 9522 is checked for SMA packets with
protocol ID 6069. The packet content is output in JSON format.
Packets with wrong protocol ID are dropped.

Content of field "StateDetail"
    valid SMA packet                        ("None")
    OSError: socket timeout                 ("timed out")
    other OSError                           (depends on error)
    wrong ID string                         ("NonSMA Packet")
    wrong protocol ID after one retry       ("Wrong PID")
    missing or wrong serial number          ("Wrong Serial Number")

Input argument: HM20 serial number in decimal format

Last change: Mar 22, 2022

@author: TomE
"""

import sys
from ctypes import BigEndianStructure, c_uint8, c_uint16, c_uint32, c_uint64, c_char
import json
from dataclasses import dataclass
import socket


MULTICAST_GRP = '239.12.255.254'
MULTICAST_PORT = 9522


class RetryException(Exception):
    """Exception used to signal that SMA packet needs to be reread."""


@dataclass
class SMApacket(BigEndianStructure):
    """Expected structure of received SMA packet.

    Valid for SMA Home Manager 2.0, Firmware revision >= 2.03.4.R

    Tot: Sum over all phases
    L1, L2, L3: Power phases

    pIn / pOut: Power  reveived / delivered
    eIn / eOut: Energy received / delivered
    act: active   power / energy [0.1 W   / 1 Ws]
    rea: reactive power / energy [0.1 Var / 1 Vars]
    app: apparent power / energy [0.1 VA  / 1 VAs]

    cosPhi: Power factor
    Freq: Frequency

    """

    _pack_ = 1      # suppress padding
    _fields_ = [

        # Header
        ('IDstring',     c_char * 4),    # "S" "M" "A" 0x00
        ('DataLength',   c_uint16),      # 0x0004
        ('Tag0',         c_uint16),      # 0x02A0, tag0 ???
        ('Group',        c_uint32),      # 0x00000001, group 1
        ('DataSize',     c_uint16),      # 0x024C: 588 bytes incl group and data
        # size field, but wo FW Rev ???
        ('Tag1',         c_uint16),      # 0x0010: „SMA Net 2“, Version 0
        ('ProtocolID',   c_uint16),      # 0x6069: Emeter protocol
        ('SUSyID',       c_uint16),      # 0x0174 (373d)
        ('Serial',       c_uint32),      # Serialnumber in Hex
        ('ticker',       c_uint32),      # ms since start

        # Total power and energy
        ('Obis_01_04',   c_uint32), ('Tot_pIn_act',  c_uint32),  # active in
        ('Obis_01_08',   c_uint32), ('Tot_eIn_act',  c_uint64),

        ('Obis_02_04',   c_uint32), ('Tot_pOut_act', c_uint32),  # active out
        ('Obis_02_08',   c_uint32), ('Tot_eOut_act', c_uint64),

        ('Obis_03_04',   c_uint32), ('Tot_pIn_rea',  c_uint32),  # reactive in
        ('Obis_03_08',   c_uint32), ('Tot_eIn_rea',  c_uint64),

        ('Obis_04_04',   c_uint32), ('Tot_pOut_rea', c_uint32),  # reactive out
        ('Obis_04_08',   c_uint32), ('Tot_eOut_rea', c_uint64),

        ('Obis_09_04',   c_uint32), ('Tot_pIn_app',  c_uint32),  # apparent in
        ('Obis_09_08',   c_uint32), ('Tot_eIn_app',  c_uint64),

        ('Obis_10_04',   c_uint32), ('Tot_pOut_app', c_uint32),  # apparent out
        ('Obis_10_08',   c_uint32), ('Tot_eOut_app', c_uint64),

        ('Obis_13_04',   c_uint32), ('Tot_cosPhi',   c_uint32),
        ('Obis_14_04',   c_uint32), ('Tot_Freq',     c_uint32),

        # Phase 1 power and energy
        ('Obis_21_04',   c_uint32), ('L1_pIn_act',   c_uint32),
        ('Obis_21_08',   c_uint32), ('L1_eIn_act',   c_uint64),

        ('Obis_22_04',   c_uint32), ('L1_pOut_act',  c_uint32),
        ('Obis_22_08',   c_uint32), ('L1_eOut_act',  c_uint64),

        ('Obis_23_04',   c_uint32), ('L1_pIn_rea',   c_uint32),
        ('Obis_23_08',   c_uint32), ('L1_eIn_rea',   c_uint64),

        ('Obis_24_04',   c_uint32), ('L1_pOut_rea',  c_uint32),
        ('Obis_24_08',   c_uint32), ('L1_eOut_rea',  c_uint64),

        ('Obis_29_04',   c_uint32), ('L1_pIn_app',   c_uint32),
        ('Obis_29_08',   c_uint32), ('L1_eIn_app',   c_uint64),

        ('Obis_30_04',   c_uint32), ('L1_pOut_app',  c_uint32),
        ('Obis_30_08',   c_uint32), ('L1_eOut_app',  c_uint64),

        ('Obis_31_04',   c_uint32), ('L1_current',   c_uint32),
        ('Obis_32_04',   c_uint32), ('L1_voltage',   c_uint32),
        ('Obis_33_04',   c_uint32), ('L1_cosPhi',    c_uint32),

        # Phase 2 power and energy
        ('Obis_41_04',   c_uint32), ('L2_pIn_act',   c_uint32),
        ('Obis_41_08',   c_uint32), ('L2_eIn_act',   c_uint64),

        ('Obis_42_04',   c_uint32), ('L2_pOut_act',  c_uint32),
        ('Obis_42_08',   c_uint32), ('L2_eOut_act',  c_uint64),

        ('Obis_43_04',   c_uint32), ('L2_pIn_rea',   c_uint32),
        ('Obis_43_08',   c_uint32), ('L2_eIn_rea',   c_uint64),

        ('Obis_44_04',   c_uint32), ('L2_pOut_rea',  c_uint32),
        ('Obis_44_08',   c_uint32), ('L2_eOut_rea',  c_uint64),

        ('Obis_49_04',   c_uint32), ('L2_pIn_app',   c_uint32),
        ('Obis_49_08',   c_uint32), ('L2_eIn_app',   c_uint64),

        ('Obis_50_04',   c_uint32), ('L2_pOut_app',  c_uint32),
        ('Obis_50_08',   c_uint32), ('L2_eOut_app',  c_uint64),

        ('Obis_51_04',   c_uint32), ('L2_current',   c_uint32),
        ('Obis_52_04',   c_uint32), ('L2_voltage',   c_uint32),
        ('Obis_53_04',   c_uint32), ('L2_cosPhi',    c_uint32),

        # Phase 3 power and energy
        ('Obis_61_04',   c_uint32), ('L3_pIn_act',   c_uint32),
        ('Obis_61_08',   c_uint32), ('L3_eIn_act',   c_uint64),

        ('Obis_62_04',   c_uint32), ('L3_pOut_act',  c_uint32),
        ('Obis_62_08',   c_uint32), ('L3_eOut_act',  c_uint64),

        ('Obis_63_04',   c_uint32), ('L3_pIn_rea',   c_uint32),
        ('Obis_63_08',   c_uint32), ('L3_eIn_rea',   c_uint64),

        ('Obis_64_04',   c_uint32), ('L3_pOut_rea',  c_uint32),
        ('Obis_64_08',   c_uint32), ('L3_eOut_rea',  c_uint64),

        ('Obis_69_04',   c_uint32), ('L3_pIn_app',   c_uint32),
        ('Obis_69_08',   c_uint32), ('L3_eIn_app',   c_uint64),

        ('Obis_70_04',   c_uint32), ('L3_pOut_app',  c_uint32),
        ('Obis_70_08',   c_uint32), ('L3_eOut_app',  c_uint64),

        ('Obis_71_04',   c_uint32), ('L3_current',   c_uint32),
        ('Obis_72_04',   c_uint32), ('L3_voltage',   c_uint32),
        ('Obis_73_04',   c_uint32), ('L3_cosPhi',    c_uint32),

        # Firmware revision
        ('Code_90_00',   c_uint32),
        ('Major',        c_uint8), ('Minor',        c_uint8),
        ('Build',        c_uint8), ('Revision',     c_char),

        # End string
        ('EndString',    c_uint32)
    ]


class DecodeSMA:
    """Decode the multicast data sent by SMA 'Home Manager 20'."""

    HM20 = {}  # dictionary for emeter data

    def __init__(self):
        """Initialize some HM20 dictionary fields to default values."""
        #
        self.HM20['HM20state'] = 'OFFLINE'
        self.HM20['DataValid'] = 'INVALID'
        self.HM20['StateDetail'] = 'None'

    def decode_header(self, data, s_number):
        """Decode header data from packet structure.

        Packet content         HM20state  DataValid  stateDetail          Action
        Invalid SMA header:    OFFLINE    INVALID    NonSMA Packet        Exit
        Invalid serial number: ONLINE     INVALID    Wrong Serial Number  Exit
        Wrong protocol ID:     ONLINE     INVALID    WrongPID             Retry
        Correct header and ID: ONLINE     VALID      None                 Exit

        """
        #  Check ID string, packet identifier and serial number
        if ((data.IDstring.decode("ascii") == 'SMA') and
            ((f'{data.ProtocolID:x}') == '6069') and
                (str(data.Serial) == s_number)):

            # Fill dictionary with header data
            self.HM20['HM20state'] = 'ONLINE'
            self.HM20['DataValid'] = 'VALID'
            self.HM20['StateDetail'] = 'None'
            self.HM20['IDstring'] = data.IDstring.decode("ascii")
            self.HM20['ProtocolID'] = f'{data.ProtocolID:x}'
            self.HM20['DataSize'] = data.DataSize
            self.HM20['Serial'] = data.Serial
            self.HM20['SUSyID'] = f'{data.SUSyID:x}'
            self.HM20['FWrev'] = ('{:d}.{:d}.{:d}.{:s}'.
                                  format(data.Major,
                                         data.Minor,
                                         data.Build,
                                         data.Revision.decode('ascii')))

            # Fill dictionary with phase data
            for i in ['Tot', 'L1', 'L2', 'L3']:
                self.decode_phase(data, i)
            return

        # Something went wrong, do some additional checks to provide more
        # information in field 'StateDetail'
        if data.IDstring.decode("ascii") != 'SMA':
            self.HM20['StateDetail'] = 'NonSMA Packet'
            return

        # Fill dict with some more information
        self.HM20['HM20state'] = 'ONLINE'
        self.HM20['IDstring'] = data.IDstring.decode("ascii")
        self.HM20['ProtocolID'] = f'{data.ProtocolID:x}'

        # Protocol ID ok?
        if (f'{data.ProtocolID:x}') != '6069':
            # try to get the next packet with correct protocol ID
            self.HM20['StateDetail'] = 'WrongPID'
            raise RetryException

        # ID string and protocol ID are ok, so serial number must be wrong
        # store the number found
        self.HM20['Serial'] = data.Serial
        self.HM20['StateDetail'] = 'Wrong Serial Number'

    def decode_phase(self, p_data, phase):
        """Decode phase data from structure and fill dict with adjusted values.

        act: active   power / energy [W   / kWh]
        rea: reactive power / energy [Var / kVarh]
        app: apparent power / energy [VA  / kVAh]

        """
        #
        p_list = ('_pIn_act', '_pIn_rea', '_pIn_app',
                  '_pOut_act', '_pOut_rea', '_pOut_app')
        e_list = ('_eIn_act', '_eIn_rea', '_eIn_app',
                  '_eOut_act', '_eOut_rea', '_eOut_app')

        for (i, j) in zip(p_list, e_list):
            self.HM20[phase + i] = getattr(p_data, (phase + i)) / 10
            self.HM20[phase + j] = getattr(p_data, (phase + j)) / 3600000

        # Content of 'Total' block differs a little bit from 'Phase' blocks
        if phase == "Tot":
            for i in ('_cosPhi', '_Freq'):
                self. HM20[phase + i] = getattr(p_data, (phase + i)) / 1000

        else:
            for i in ('_current', '_voltage', '_cosPhi'):
                self. HM20[phase + i] = getattr(p_data, (phase + i)) / 1000


class MultiCastReception():
    """Handle multicast reception of SMA packets."""

    def __init__(self, mc_grp, mc_port):
        """Initialize packet reception from multicast port."""
        #
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
                                  socket.IPPROTO_UDP)

        # socket.setsockopt(level, optname, value: int)
        # Allow reuse of socket (to allow another instance of python running
        # this script binding to the same ip/port)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        self.sock.bind(("", mc_port))

        self.mreq = (socket.inet_aton(mc_grp)
                     + socket.inet_aton(str(socket.INADDR_ANY)))

    def join_multicast(self):
        """Join multicast group."""
        #
        self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
                             self.mreq)
        self.sock.settimeout(0.5)  # 500 ms allow reception of two packets

    def close_multicast(self):
        """Close connection."""
        #
        self.sock.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP,
                             self.mreq)
        self.sock.close()


def main():
    """."""
    #
    try:
        # Extract serial number from command line arguments
        serial_number = 0
        if (len(sys.argv) == 2 and sys.argv[1].isdigit()):
            serial_number = sys.argv[1]

        # Init packet decoding
        sma = DecodeSMA()

        # Init packet reception from multicast port
        mcr = MultiCastReception(MULTICAST_GRP, MULTICAST_PORT)
        mcr.join_multicast()

        for i in range(2):      # allow for one retry

            # Instantiate packet class here to get
            # an empty SMA structure in each loop
            packet = SMApacket()

            # receive data
            mcr.sock.recv_into(packet)

            # decode data
            try:
                sma.decode_header(packet, serial_number)
                break

            except RetryException:      # retry because of wrong protocol id
                pass

        # Clean up after decode
        mcr.close_multicast()

    except OSError as error:        # e.g. wrong multicast group or packet size
        sma.HM20['StateDetail'] = 'OSError: ' + str(error)

    finally:
        # Finally dump data in json format
        print(json.dumps(sma.HM20))
        #  print(json.dumps(sma.HM20, indent=4))


if __name__ == '__main__':
    main()

Some points worth to mention:

  1. I can and will not guarantee that the script works in all systems. It has been tested in my system standalone on WIN 10 and both standalone on Debian bullseye and integrated in a openHAB 3.2 / DSL rule. I also can’t give any support with UI based integration since I use text based DSL rules only.
  2. The script doesn’t support multiple Home Manager entities.
  3. I strongly recommend to test the function standalone first and than in a test (not productive) environment.
  4. If you feel not comfortable with python or scripts in general I recommend to use the current binding and wait for an update. The wrong data can be filtered in a rule or by means of a js transformation.

Standalone usage

To start the script standalone on the command line type (add your HM serial number):

/usr/bin/python3 sma_emeter.py 0123456789

Integration in openHAB

1. Items:

Group gHomeManager

String DataValid													(gHomeManager)
String HM20state													(gHomeManager)
String IDstring														(gHomeManager)
String ProtocolID  													(gHomeManager)
Number DataSize  													(gHomeManager)
String SUSyID  			"HM SUSy-ID"								(gHomeManager)
Number Serial  			"HM Serialnumber"							(gHomeManager)
String FWrev			"FW revision [%s]"							(gHomeManager)
Number Tot_pIn_act  	"Bezogene Leistung [%.3f W]"				(gHomeManager)
Number Tot_pIn_rea  	"Bezogene Blindleistung [%d Var]"			(gHomeManager)
Number Tot_pIn_app  	"Bezogene Scheinleistung [%d VA]"			(gHomeManager)
Number Tot_eIn_act  	"Bezogene Wirkenergie [%d kWh]"				(gHomeManager)
Number Tot_eIn_rea  	"Bezogene Blindenergie [%d kVarh]"			(gHomeManager)
Number Tot_eIn_app  	"Bezogene Scheinenergie [%d kVAh]"			(gHomeManager)
Number Tot_pOut_act  	"Eingespeiste Leistung [%.3f W]"			(gHomeManager)
Number Tot_pOut_rea  	"Eingespeiste Blindleistung [%d Var]"		(gHomeManager)
Number Tot_pOut_app  	"Eingespeiste Scheinleistung [%d VA]"		(gHomeManager)
Number Tot_eOut_act  	"Eingespeiste Wirkenergie [%d kWh]"			(gHomeManager)
Number Tot_eOut_rea  	"Eingespeiste Blindenergie [%d kVarh]"		(gHomeManager)
Number Tot_eOut_app  	"Eingespeiste Scheinenergie [%d kVAh]"		(gHomeManager)
Number Tot_cosPhi  		"cosinus Phi [%.3f]"						(gHomeManager)
Number Tot_Freq  		"Frequenz [%.3f Hz]"						(gHomeManager)
Number L1_pIn_act  		"L1 Bezogene Wirkleistung [%d W]"			(gHomeManager)
Number L1_pIn_rea  		"L1 Bezogene Blindleistung [%d Var]"		(gHomeManager)
Number L1_pIn_app  		"L1 Bezogene Scheinleistung [%d VA]"		(gHomeManager)
Number L1_eIn_act  		"L1 Bezogene Wirkenergie [%d kWh]"			(gHomeManager)
Number L1_eIn_rea  		"L1 Bezogene Blindenergie [%d kVarh]"		(gHomeManager)
Number L1_eIn_app 		"L1 Bezogene Scheinenergie [%d kVAh]"		(gHomeManager)
Number L1_pOut_act	 	"L1 Eingespeiste Wirkleistung [%d W]"		(gHomeManager)
Number L1_pOut_rea	 	"L1 Eingespeiste Blindleistung [%d Var]"	(gHomeManager)
Number L1_pOut_app	 	"L1 Eingespeiste Scheinleistung [%d VA]"	(gHomeManager)
Number L1_eOut_act	 	"L1 Eingespeiste Wirkenergie [%d kWh]"		(gHomeManager)
Number L1_eOut_rea	 	"L1 Eingespeiste Blindenergie [%d kVarh]"	(gHomeManager)
Number L1_eOut_app	 	"L1 Eingespeiste Scheinenergie [%d kVAh]"	(gHomeManager)
Number L1_current 		"L1 Strom [%.3f A]"							(gHomeManager)
Number L1_voltage 		"L1 Spannung [%.3f V]"						(gHomeManager)
Number L1_cosPhi		"L1 cosinus Phi [%.3f]"						(gHomeManager)
Number L2_pIn_act 		"L2 Bezogene Wirkleistung [%d W]"			(gHomeManager)
Number L2_pIn_rea 		"L2 Bezogene Blindleistung [%d Var]"		(gHomeManager)
Number L2_pIn_app 		"L2 Bezogene Scheinleistung [%d VA]"		(gHomeManager)
Number L2_eIn_act 		"L2 Bezogene Wirkenergie [%d kWh]"			(gHomeManager)
Number L2_eIn_rea 		"L2 Bezogene Blindenergie [%d kVarh]"		(gHomeManager)
Number L2_eIn_app		"L2 Bezogene Scheinenergie [%d kVAh]"		(gHomeManager)
Number L2_pOut_act 		"L2 Eingespeiste Wirkleistung [%d W]"		(gHomeManager)
Number L2_pOut_rea 		"L2 Eingespeiste Blindleistung [%d Var]"	(gHomeManager)
Number L2_pOut_app		"L2 Eingespeiste Scheinleistung [%d VA]"	(gHomeManager)
Number L2_eOut_act		"L2 Eingespeiste Wirkenergie [%d kWh]"		(gHomeManager)
Number L2_eOut_rea		"L2 Eingespeiste Blindenergie [%d kVarh]"	(gHomeManager)
Number L2_eOut_app		"L2 Eingespeiste Scheinenergie [%d kVAh]"	(gHomeManager)
Number L2_current		"L2 Strom [%.3f A]"							(gHomeManager)
Number L2_voltage		"L2 Spannung [%.3f V]"						(gHomeManager)
Number L2_cosPhi		"L2 cosinus Phi [%.3f]"						(gHomeManager)
Number L3_pIn_act 		"L3 Bezogene Wirkleistung [%d W]"			(gHomeManager)
Number L3_pIn_rea 		"L3 Bezogene Blindleistung [%d Var]"		(gHomeManager)
Number L3_pIn_app 		"L3 Bezogene Scheinleistung [%d VA]"		(gHomeManager)
Number L3_eIn_act 		"L3 Bezogene Wirkenergie [%d kWh]"			(gHomeManager)
Number L3_eIn_rea 		"L3 Bezogene Blindenergie [%d kVarh]"		(gHomeManager)
Number L3_eIn_app 		"L3 Bezogene Scheinenergie [%d kVAh]"		(gHomeManager)
Number L3_pOut_act 		"L3 Eingespeiste Wirkleistung [%d W]"		(gHomeManager)
Number L3_pOut_rea 		"L3 Eingespeiste Blindleistung [%d Var]"	(gHomeManager)
Number L3_pOut_app 		"L3 Eingespeiste Scheinleistung [%d VA]"	(gHomeManager)
Number L3_eOut_act 		"L3 Eingespeiste Wirkenergie [%d kWh]"		(gHomeManager)
Number L3_eOut_rea 		"L3 Eingespeiste Blindenergie [%d kVarh]"	(gHomeManager)
Number L3_eOut_app 		"L3 Eingespeiste Scheinenergie [%d kVAh]"	(gHomeManager)
Number L3_current 		"L3 Strom [%.3f A]"							(gHomeManager)
Number L3_voltage 		"L3 Spannung [%.3f V]"						(gHomeManager)
Number L3_cosPhi		"L3 cosinus Phi [%.3f]"						(gHomeManager)

Note: The item names must be consistent with those from the JSON output to make item updates in a rule easier.

  1. Rule template
rule "HM20 Query"

when
	Time cron "* * * * * ? *"     // update data every second

then
	val JSONstring = executeCommandLine(Duration.ofMillis(700), "/usr/bin/python3", "/etc/openhab/scripts/sma_emeter.py", "0123456789")
	val DataValid = (transform("JSONPATH", "$.DataValid", JSONstring))
	val HM20status = (transform("JSONPATH", "$.HM20state", JSONstring))
	val HM20detail = (transform("JSONPATH", "$.StateDetail", JSONstring))
	
	// check status of received data
	// JSONstring === null: No data received, probably executeCommandLine timeout, set relevant item states to 0
	// HM20status == "OFFLINE": script timeout or no sma packet received, set relevant item states to 0
	// HM20status == "ONLINE" and DataValid == "INVALID": Wrong packet ID, handled in script, never seen here, DSL timeout may occur
	// HM20status == "ONLINE" and DataValid == "VALID": Valid packet, update item states
	
	if ((JSONstring !== null) && (HM20status == "ONLINE") && (DataValid == "VALID"))
	{
		// refresh all HM items 
		gHomeManager.members.forEach[i | 
		{
			i.postUpdate(transform("JSONPATH", "$." + i.name, JSONstring))
		}]

	else
	{
		logWarn("Photovoltaik.rules::HM20 Abfrage", "No valid data received, HM items set to 0. State: {}, Detail: {}, JSONstring: {} ", HM20status, HM20detail, JSONstring)
		
		// Additional error handling from here

	}

Notes:

  • Adjust the cron trigger to your needs
  • In the rule template the script is stored in /etc/openhab/scripts/. Adjust the path if needed.
  • Replace the 0123456789 with the serial number of your HM 2.0
  • The template is a simplified version. E.g. it isn’t useful to update the energy items every second. I personally prefer to use two groups ‘gHomeManagerF’ (power, voltage current and power factor items) and ‘gHomeManagerS’ (energy items) together with two item update loops. The ‘gHomeManagerF’ group items are updated every second while the second loop for the ‘gHomeManagerS’ group items is executed depending on a counter variable which is incremented every second up to 300 (5 min).
1 Like

@tomE decoding and labeling work you did looks awesome. I see multiple additional fields which we currently miss in the binding. Kudos for commitment!

Most of it I found in the net. And the remaining ones were easy to deduce. The biggest work was to type them in :slight_smile:

Unfortunately I’m getting some errors in VScode:

[{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.xbase.validation.IssueCodes.invalid_number_of_arguments",
	"severity": 8,
	"message": "Invalid number of arguments. The method executeCommandLine(String, int) is not applicable for the arguments (Object,String,String,String)",
	"startLineNumber": 7,
	"startColumn": 19,
	"endLineNumber": 7,
	"endColumn": 37
},{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.diagnostics.Diagnostic.Linking",
	"severity": 8,
	"message": "The method ofMillis(int) is undefined for the type Class<Duration>",
	"startLineNumber": 7,
	"startColumn": 47,
	"endLineNumber": 7,
	"endColumn": 55
},{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.xbase.validation.IssueCodes.incompatible_types",
	"severity": 8,
	"message": "Type mismatch: cannot convert from String to int",
	"startLineNumber": 7,
	"startColumn": 62,
	"endLineNumber": 7,
	"endColumn": 80
},{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.diagnostics.Diagnostic.Linking",
	"severity": 8,
	"message": "The method or field gHomeManager is undefined",
	"startLineNumber": 21,
	"startColumn": 3,
	"endLineNumber": 21,
	"endColumn": 15
},{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.xbase.validation.IssueCodes.too_little_type_information",
	"severity": 8,
	"message": "There is no context to infer the closure's argument types from. Consider typing the arguments or put the closures into a typed context.",
	"startLineNumber": 21,
	"startColumn": 31,
	"endLineNumber": 24,
	"endColumn": 5
},{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.xbase.validation.IssueCodes.incompatible_types",
	"severity": 8,
	"message": "Type mismatch: cannot convert from Object to String",
	"startLineNumber": 23,
	"startColumn": 4,
	"endLineNumber": 23,
	"endColumn": 5
},{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.diagnostics.Diagnostic.Linking",
	"severity": 8,
	"message": "The method or field name is undefined for the type Object",
	"startLineNumber": 23,
	"startColumn": 48,
	"endLineNumber": 23,
	"endColumn": 52
},{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.diagnostics.Diagnostic.Syntax",
	"severity": 8,
	"message": "missing '}' at 'else'",
	"startLineNumber": 27,
	"startColumn": 2,
	"endLineNumber": 27,
	"endColumn": 6
},{
	"resource": "/K:/rules/smahomemanager.rules",
	"owner": "_generated_diagnostic_collection_name_#0",
	"code": "org.eclipse.xtext.diagnostics.Diagnostic.Syntax",
	"severity": 8,
	"message": "mismatched input '}' expecting 'end'",
	"startLineNumber": 35,
	"startColumn": 2,
	"endLineNumber": 35,
	"endColumn": 3
}]

Errors solved …

There was a missing closing } for the “if” and a missing end at the end of the rule

How can one find out whether there the binding has been updated?

It is a binding that is part of the openhab distributed bindings ?
Then you can go to github and check for when latest changes where done:

For those of you who are still on 2.5

The rule of @tomE has to be modified a little:
Thats is the change from 2.5 to 3

//val JSONstring = executeCommandLine(Duration.ofMillis(700), "/usr/bin/python3", "/openhab/conf/scripts/sma_emeter.py", "3002852584")
    val JSONstring = executeCommandLine("/usr/bin/python3 /openhab/conf/scripts/sma_emeter.py 3002852584", 700)

I’m running this on OH3.2 - it works fine, however, I’m getting a repeating warning that floods my log file:

[WARN ] [ipt.Photovoltaik.rules::HM20 Abfrage] - No valid data received, HM items set to 0. State: OFFLINE, Detail: OSError: timed out, JSONstring: {"HM20state": "OFFLINE", "DataValid": "INVALID", "StateDetail": "OSError: timed out"}