Requesting working example of RFLink <-> OpenHab via MQTT

I am looking into the RFLink Gateway and how to integrate to OH2.

I have set up the RFLink Shield and ArduinoMega - and made a small MQTT gateway on an ESP8266.

So, now I am receiving MQTT messages in OH2 with raw RFLink data in it.

Does anyone have a working rules/script setup to share where you parse the raw data from RFLink into something usable ?

I have a weatherstation which I can see RFLink receives messages from, but I am struggling to place the readings into something useable - like temperature, humidity, rain, wind etc.

Currently, I am not looking to send messages back to RF-devices - just reading sensor data…

I have looked at the binding for RFLink - but that seem to expect serial input - and that is not my thing :wink:

Perhaps if you posted the format of the messages and maybe one or two relevant examples we could provide some suggestions.

Yep - here goes:

The raw message comes in one string looking like this (below are 4 seperate messages):

20;AC;Cresta;ID=3601;TEMP=00f0;HUM=57;BAT=OK;
20;AD;Cresta;ID=3601;TEMP=00ef;HUM=57;BAT=OK
20;AE;Cresta;ID=3601;TEMP=00f0;HUM=57;BAT=OK;
20;AF;Cresta;ID=8001;RAIN=05b0;BAT=OK;

the first two numbers are just an identifier (20=receive, 10 would be transmit)
the next two are an incremental hex number per message
then comes the name of the vendor of the device
then an ID, prefixed by “ID=” which I believe identifies the type of device from this vendor (or perhaps it is even more unique, I havent been able to tell)
Then som data - also comma separated - in my case here;
TEMP which comes with a 4 digit hex value (right now it is around 24 degrees here)
HUM which seem to be just the rounded % of humidity
BAT=OK which seems to say that battery is - OK ?
other examples I have seen are RAIN - but I dont know what the value tells me ? - perhaps total mm rain collected since start ?

I have started on a rule which will parse this out - but it is tedious work and I had hoped to see someone who already had done this.

OK, we are dealing with hex encoded values but do you know what sort of value they represent? Mainly I’m concerned about the TEMP and RAIN. Is it a floating point number or did they do something else (e.g. it is an integer but they expect you to divide by 10 or 100).

Assuming that the device is returning celsius I’m going to guess it is encoding one decimal place and they expect you to do the divide by 10.

This is because:

00f0 = 240
00ef = 239

If this is celsius than 24.0 degrees and 23.9 degrees are reasonable values.

HUM is almost certainly percent and we will never see anything except parsable numbers from it.

RAIN is probably expecting you to divide by something too unless you have received almost 1.5 m of rain since installing this sensor, which could be reasonable. In all likelihood, you will have to observe how this number changes over time corresponding with observations of rainfall to figure how this number is being reported.

NOTE: 05b0 = 1456

OK, let’s start parsing the values.

rule "Parse RFLink messages"
when
    Item RFLink_Messages received update
than
    // split the message into parts on the ';'
    val parts = RFLink_Messages.state.toString.split(';')

    // the parts of the message start from zero, the replace removes the ID= from the message
    val id = parts.get(3).replace("ID=",'')

    // look at the fifth part of the message to see which type of message it is and parse accordingly    
    switch parts.get(4) {

        // process a TEMP/HUM/BAT message
        case parts.get(4).startsWith("TEMP"): {
            // Get the TEMP, I broke it down into steps for legibility
            val tempHex = parts.get(4).replace("TEMP=", '')
            val rawTemp = new java.math.BigDecimal(Integer::parseInt(tempHex, 16))
            val adjustedTemp = rawTemp / 10.0

            // assumes you named your Temp Item for this device 3601_Temp, you can do some additional 
            // code to change the 3601 to something more friendly but the point is we use the id to construct
            // the name of the Item to update.
            postUpdate(id+"_Temp", adjustedTemp.toString)

            // Get the Humidity, all on one line for brevity
           val humidity = new java.math.BigDecimal(Integer::parseInt(parts.get(5).replace("HUM=", ''), 16))
           postUpdate(id+"_Hum", humidity.toString)

            // Get the Battery
            val bat = if(parts.get(5) == "BAT=OK") "ON" else "OFF"
            postUpdate(id+"_Bat", bat)
        }

        // process a RAIN/BAT message
        case parts.get(4).startsWith("RAIN") {
            // Get the rain
           val rain = java.math.BigDecimal(Integer::parseInt(parts.get(4).replace("RAIN=", ''), 16))
           // do more math based on what this number represents
           postUpdate(id+"_Rain", rain.toString)

           // Get the battery
           val bat = if(parts.get(5) == "BAT=OK") "ON" else "OFF"
           postUpdate(id+"_Bat", bat)
        }
        default: lofWarn("RFLink", "Received an unknown message type: " + RFLink_Messages.state.toString)
    }
end

I just typed in the above and there are likely errors. Dealing with numerical values like I have is prone to cause the Rules engine problems. I tried to write it in such a way as to avoid most of the potential pitfalls but I could have caused problems.

If it doesn’t work be sure to look at the logs for errors and add lots of logging to find the line it doesn’t like and what everything is equal to.

One final question, how quickly do these messages come in?

One final note to others on the forum. This is the one case where I think it is acceptable and appropriate to use the Actions for sendCommand and postUpdate instead of the Item methods. We don’t have a reference to the Items and there is no need to go through the extra work of pulling them out of a Group like you’ve seen me do all over the place.

2 Likes

Hi, here is a link to the RFLink protocol that is useful to decode the messages.
http://www.rflink.nl/blog2/protref
I’m using a python script to decode the messages, format them into JSON messages and sending them via MQTT to openHAB.
The python script:

# -*- coding: utf-8 -*-
import sys
from serial import *
import io
import struct
import binascii
from bitstring import *
import time
import json
from datetime import datetime
import signal

localtime   = time.localtime()
timeString  = time.strftime("%Y%m%d%H%M%S", localtime)

import paho.mqtt.client as paho

DEBUG = True #False

RECONNECT_DELAY_SECS = 2

def on_connect(mqttc, userdata, flags, rc):
    if DEBUG:
        dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
        print('%s Connect %s %s %s' % (dt, str(userdata), str(flags), str(rc) ))
    return

def on_disconnect(client, userdata, rc):
    if rc != 0:
        dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
        print("%s Unexpected disconnection. RC = %s" % (dt, rc))
    while rc != 0:
        client.reinitialise(client_id="rflinkgateway")
        time.sleep(RECONNECT_DELAY_SECS)
        dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
        print("%s Reconnecting..." % (dt))
        try:
            signal.alarm(5)
            rc = client.connect("pine64.fritz.box", keepalive=30)
            signal.alarm(0)
        except Exception as e:
            dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
            print('%s Exception on reconnect' % (dt))
            print(e)
            raise e
        if rc != 0:
            dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
            print("%s Reconnect failed. RC = %s" % (dt, rc))
        client.loop_start()
    return

def on_publish(client, userdata, mid):
    return

def main():
    dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
    print("%s Start main" % (dt))

    ser = Serial(port='/dev/ttyACM0',baudrate=57600,timeout=4)
    sio = io.TextIOWrapper(io.BufferedRWPair(ser, ser))
    mqttc = paho.Client(client_id="rflinkgateway")
    mqttc.on_connect = on_connect
    mqttc.on_disconnect = on_disconnect
    mqttc.on_publish = on_publish
    mqttc.connect("pine64.fritz.box", keepalive=30)
    mqttc.loop_start()
    while True:
        try:
            line = sio.readline()
            if len(line) > 0:
                if DEBUG:
                    dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
                    print("%s <%s>:%s" % (dt, len(line), line))
                if ';' in line:
                    _parts = line.split(';')
                    if DEBUG:
                        dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
                        _p = str(_parts)
                        print('%s %s' % (dt, _p))
                    if _parts[0] != '20':
                        continue
                    if _parts[2].startswith('Nodo RadioFrequencyLink'):
                        continue
                    if _parts[2] == 'Alecto V5':
                        if 'ID' in _parts[3]:
                            if _parts[3].split('=')[1] != '00c0':
                                continue
                        else:
                            continue
                        if '=' not in _parts[4]:
                            continue
                        if _parts[4].split('=')[0] != 'TEMP':
                            continue
                        _tempx = _parts[4].split('=')[1]
                        _temp = '0x'+ _tempx
                        a = BitStream(_temp)
                        temp = a.read("intbe:16")
                        if temp < 0:
                            temp = temp & 0b0111111111111
                            temp = temp * -1
                        if '=' not in _parts[5]:
                            continue
                        if _parts[5].split('=')[0] != 'RAIN':
                            continue
                        _rainx = _parts[5].split('=')[1]
                        _rain = '0x'+ _rainx
                        a = BitStream(_rain)
                        rain = a.read("intbe:16")
                        if DEBUG:
                            dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
                            tempstr = "%s %1.1f C %1.1f mm" % (dt, temp/10, rain/10)
                            print(tempstr)
                        data = {}
                        data['temp'] = temp/10
                        data['rain'] = rain/10
                        payload = json.dumps(data)
                        (result, mid) = mqttc.publish('/sensors/weather', payload, qos=1)
                    if _parts[2] == 'Kaku':
                        if '=' not in _parts[5]:
                            continue
                        if _parts[5].split('=')[0] != 'CMD':
                            continue
                        if _parts[3].split('=')[0] != 'ID':
                            continue
                        if _parts[3].split('=')[1] != '44':
                            continue
                        _cmd = _parts[5].split('=')[1]
                        if _cmd == 'ON':
                            _status = 'CLOSED'
                        elif _cmd == 'OFF':
                            _status = 'OPEN'
                        else:
                            continue
                        _data = {}
                        _data['status'] = _status
                        payload = json.dumps(_data)
                        (result, mid) = mqttc.publish('/sensors/window/office', payload, qos=1)
                    if _parts[2] == 'Oregon Rain2':
                        if '=' not in _parts[5]:
                            continue
                        if _parts[3].split('=')[0] != 'ID':
                            continue
                        if _parts[3].split('=')[1] != '2A68':
                            continue
                        if _parts[5].split('=')[0] != 'RAIN':
                            continue
                        _rainx = _parts[5].split('=')[1]
                        _rain = '0x'+ _rainx
                        a = BitStream(_rain)
                        rain = a.read("intbe:16")
                        if DEBUG:
                            dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
                            tempstr = "%s %1.1f mm" % (dt, rain/10)
                            print(tempstr)
                        data = {}
                        data['rain'] = rain/10
                        payload = json.dumps(data)
                        (result, mid) = mqttc.publish('/sensors/rain', payload, qos=1)
            continue
        except KeyboardInterrupt:
            mqttc.disconnect()
            mqttc.loop_stop()
            ser.close()
            return False
        continue
if __name__ == "__main__":
    if len(sys.argv) < 2:
        while True:
            try:
                result = main()
                dt = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.now())
                print("%s Return from main RC=%s" % (dt, str(result)))
                if result is False:
                    break
            except Exception as e:
                print(e)
                raise
    else:
        sensor = sys.argv[1]
        temp = sys.argv[2]
        publish(sensor,temp)

and the relevant definition of an item:

Number Niederschlag            "Niederschlag [%.1f mm]"	<rain> (Weather) {mqtt="<[mosquitto:/sensors/rain:state:JSONPATH($.rain)]"}
1 Like

Thanks @rlkoshak and @Jue_Wis - this is great input. I am building this as rules in Openhab parsing and evaluating from the raw data which is sent over from RFLink by MQTT. I will post a working setup when I am through…

Right now I need some help regarding calculation of difference - but will post a seperate question on that.

Thanks @rlkoshak. My preference remain to keep it simple as possible and for this reason I like the way to receive RFLink data directly from serial binding. Your parsing instruction was a great help for me to decode correctly both my temperature sensor and rollershutter command confirmation. However I’m still trying to send command to RFLink module to control that rollershutter. Command to be sent on the serial port is similar to the information received on it. One example to start an upper movement is: 10;BrelMotor;f377cc;05;UP;

Could you give me some hint about how to initiate a bi-directionnal communication with that serial port?
My current item is:

String RFLinkRaw	"RFLink raw data [%s]" <text> { serial="/dev/ttyUSB0@57600" }

Do I need to create a new item for the output string?

I don’t use the serial binding so I can no idea how or if this is even possible. I know Rules but not every binding.

Don’t know, if the native RFLink binding is in good shape and working on the recent milestone builds.
But what you are trying to do “by hand” usually should happen in the binding.

Maybe you should reach out to the developers there and ask for help if some feature is missing (or even help coding if you are a developer :wink: ). But parsing the temp/hum/etc. is supported already:

RFLink binding currently supports following types of devices:

  • Energy
  • Lighting switch
  • Rain ( to be tested )
  • RTS / Somfy blinds (Send)
  • Temperature (Receive)
  • Humidity (Receive)
  • Temperature and Humidity (Receive)
  • Wind ( to be tested )
  • MiLight RGB light (Send/Receive)i* X10 Switch (Send/Receive)
  • AB400D Elro Switch (Send)
  • X10Secure Contact (Receive)
  • Other simple RFLink switches (Send/Receive)

As I said, not sure if it is still working on the recent openHAB.

I think it definitely deserves to get improved and probably merged to the “official” bindings someday.

Here is one link, not sure if this is the most recent one or if some fork is more complete:

ok, thanks for the information, I’ll take a look at the binding from that link.

I use D Martin’s RFLinkGateway - decodes the message into either a single mqtt message, or multiple messages depending on config:

Not able to post a detailed example (not at my PC), but found it straight forward to install and configure.

Cheers
James

Hello @rlkoshak, I based parsing of raw data received from RFLink serial bus directly from your recommandation. Temperature was decode perfectly until… cold temperature. In fact, I don’t know how to manage minus temperature (signed value). Temperature is encode in HEX on 2 bytes as per your example. So for example, 0x0066 mean 102 (10,2 C). If temperature is -10.2C, raw value received is 0x8066. Currently decoding displayed that data as 3287,0C. How to manage signed value? See below my current code:

thanks !

rule "RFLink IN"
when
    Item RFLinkRaw received update
then
    logInfo(logName, "raw data received: " + RFLinkRaw.state)

// split the message into parts on the ';'
val RFbuffer = RFLinkRaw.state.toString.split(';')
//	val RFprefix = RFbuffer.get(0)
//	val RFstamp = RFbuffer.get(1)
val RFvendor = RFbuffer.get(2)
val RFidLong = RFbuffer.get(3)
val RFdata_1 = RFbuffer.get(4)
val RFdata_2 = RFbuffer.get(5)
val RFidHex = transform("REGEX", "ID=([0-9a-fA-F]*)", RFidLong)


// look at the fifth part of the message to see which type of message it is and parse accordingly    
switch (RFvendor + " " + RFidHex) {

    // process a TEMP message
    case "LacrosseV4 0001" : { //outside Lacrosse sensor 
    	val tempHex = RFdata_1.replace("TEMP=", '')
        val rawTemp = new java.math.BigDecimal(Integer::parseInt(tempHex, 16))
        val adjustedTemp = rawTemp / 10.0	
        Lacrosse_OUT_Temperature.postUpdate(adjustedTemp)
        logInfo(logName, "some data received from " + RFvendor + ":Temp=" + adjustedTemp + "C" )
    }
    // process a RollerShutter message
    case "BrelMotor f377cc" : {
		logInfo(logName, "Command confirmation from " + RFvendor + ":" + RFdata_2)
	}
	default : logInfo(logName, "completely irrelevant data received from " + RFvendor)
 }
end

I honestly don’t know. I don’t do raw parsing of values like this that much. You may need to process each number/letter from the String individually and add them all together.

Or maybe there is a way to parse it into a byte or a short and it will treat that first bit as the sign. Do some Google searching for how to convert hex to a number in Java and it will apply here.

I tweak my code like this. It is working for both positive and negative temperature, but that code is not really nice. If someone know how to do it the right way, just let me know.

case "LacrosseV4 0001" : {
        val tempHex = RFdata_1.replace("TEMP=", '')
        val rawTemp = new java.math.BigDecimal(Integer::parseInt(tempHex, 16))            
        val adjustedNegTemp = (rawTemp-32768)/-10.0
        val adjustedPosTemp = rawTemp / 10.0
         
        if (rawTemp > 32768) {
        	Lacrosse_OUT_Temperature.postUpdate(adjustedNegTemp)
        	logInfo(logName, "some data received from " + RFvendor + ":Temp=" + adjustedNegTemp + "C" )
        }
        else {
        	Lacrosse_OUT_Temperature.postUpdate(adjustedPosTemp)
        	logInfo(logName, "some data received from " + RFvendor + ":Temp=" + adjustedPosTemp + "C" )
        }
    }