OpenHAB CULfw for Somfy RTS Rollershutters

I found I weakness in my code. There is a very special behavior of Openhab. If I set my Number Item in a rule with:

rollingNumber_AllShutter.postUpdate(2 + (rollingNumber_AllShutter.state as DecimalType))

it stays INTEGER. E.g. 2+10 gives 12.

But if I do a reboot of the Raspberry it changes to DOUBLE or FLOAT: 12.0

If I now try to parse the string “12.0” to INTEGER in Python it fails. There for I had to update my calling code to:

val result = executeCommandLine(Duration.ofSeconds(2),"python", "/etc/openhab/scripts/OpenAllShutter.py", (rollingNumber_AllShutter.state as DecimalType).intValue.toString)

    rollingNumber_AllShutter.postUpdate(2 + (rollingNumber_AllShutter.state as DecimalType))

    logInfo("Python-Cul",result)

Anyhow over the weekend, I will post a hole tutorial how to set up the shutters from the scratch, including needed modification of the Openhap installation. (E.g. Open additional packages)

1 Like

wow! that would be really great. many thanks in advance!

maybe try rollingNumber_AllShutter.state as Number

probably rollingNumber_AllShutter.state returns NULL after reboot. try to set an intial value with a startup rule

No this is not the case. I choose to store the rolling code in an Item, because of this. The Item state is set to persist.

as Number is not working the log shows:

File "/etc/openhab/scripts/OpenAllShutter.py", line 19, in <module>

    Introllingcode=int(args.rollingCode)

ValueError: invalid literal for int() with base 10: '269.0'

So there is a value, but it is no longer INTEGER after restart.

that does not sound correct to me if you want the result to be integer. what type is your item?

As promised here some kind of tutorial how I set up my shutters:
I have openHABian 3.0.1 on a Raspberry Pi running.

Some generell command:

  • The RTS protocol used by Somfy is a one way protocol. This means you can only send commands to the motor, but there will never be any feedback.
    *Programming the motors only means that the controller in the shutter will add/remove the address from the list of addresses the motor will react if a command is send
  • One Adresse can be used with different shutters at the same time
  • The so called RollingCode is just a number, telling the motor which command is the current one to execute
  • This is to avoid double execution of commands, but one has to keep the RollingCode up to date

I did it without any binding, but via the Python script.
Therefore the gnu.io is required. To do so I installed it, via the karaf console.

Therefore I connect to my OpenHAB and opened a second ssh conection to the karaf console

ssh -p 8101 openhab@localhost

The standard passphrase is habopen.
Running the command

feature:install openhab-transport-serial

will install the package. To prevent the reset of the serial connection, I added the modifications suggested by Chacha

The definition of my items, is in the item File under /etc/openhab/items here I have a file named Meine_Items.items. The file name is free of choice, but the ending must be items.

Switch CUL_PROG_SWITCH "Shutter Programmieren" <switch>


Number rollingNumber_Arbeitszimmer "Befehlcounter Arbeitszimmer" <CULRollingNumber> (RollerCommandCounter)

the code to Python code to program a motor is in the file NewShutter.py stored under /etc/openhab/scripts

#!/usr/bin/python
import serial
import argparse



# Baudrate used by Somfy
BaudRate = 38400  #In my case 38400 but I found 9600, too. But it was not working so it is just try and error

# Fixed Address the shutter will be programmed, Choose a unique one, for each Shutter or set of Shutters
address='00001A' 
#Encryptionkey Somfy
encryptionKey = 'A1'

#Schnittstelle zum CUL
ser = serial.Serial('/dev/ttyUSB0', BaudRate, timeout=.1)

#Current RollingCode -> 0 if this is a new Shutter
INTRollingCode = 0
#Parse RollingCode to integer
STRRollingCode=hex(INTRollingCode)[2:].zfill(4).upper()
# Command (1 = My, 2 = Up, 4 = Down, 8 = Prog)
C="8"

command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)
ser.close()

If you program a new motor, the Address in the NewShutter.py must be a new one as well as the Number rollingNumber_Arbeitszimmer "Befehlcounter Arbeitszimmer" <CULRollingNumber> (RollerCommandCounter) must be replace by a new one, with a different name.
Take care of your Addresses and the corresponding number items

The python script is triggered by a rule, stored under /etc/openhab/rules in file with the ending .rules

rule "PROG SHUTTER"
    when
	Item CUL_PROG_SWITCH received command ON
    then
     logInfo("Python-Cul","Program")
    val result =executeCommandLine(Duration.ofSeconds(1),"python", "/etc/openhab/scripts/NewShutter.py")
    rollingNumber_Arbeitszimmer.postUpdate(1)
    logInfo("Python-Cul",result)
end

In this rule you must put the number item in, which later keeps the rolling counter. I use the HabPanel. I just put the switch there and let it change to ON. The Switch will not appear under the screen at the PaperUI. The reason is, that there is no Channel connected. You have to put it to a HabPanel or the BasicUI.

To trigger the programming, one must start the process by pressing the PROG Button on the remote. Keep it pressed until the shutter move up and down. Than press the Button. The shutters should now move up and down, too. If this is not the case:

  • The Port is not /dev/ttyUSB0: Try different ports like /dev/ttyUSB1 or /dev/ttyUSB2
  • Maybe the Baudrate is wrong: So check in the Python file.
  • The Address was used before and the motor has a different RollingCode: This is hard to find; maybe it is better to just choose a different Address

To move my shutters now. I have one script to open them. To make them move up completely I send the command twice. If I send the command only once, my shutters only move a small step.

#!/usr/bin/python
import serial
import argparse
import time

parser = argparse.ArgumentParser(description="Sends SOMFY RTS commands with the nanoCUL on the selected port")
parser.add_argument("RollingCode", help="The RollingCode", type=str)
args = parser.parse_args()

# Address you used to program the shutter before
address='00000A'
#Encryptionkey Somfy
encryptionKey = 'A1'

# Baudrate used by Somfy
BaudRate = 38400  #In my case 38400 but I found 9600, too. But it was not working so it is just try and error

#Schnittstelle zum CUL
ser = serial.Serial('/dev/ttyUSB0', BaudRate, timeout=.1)

#Parse RollingCode to integer
Introllingcode=int(args.rollingCode)

# Command (1 = My, 2 = Up, 4 = Down, 8 = Prog)
C="2"

#Parse integer RollingCode to HEX String with length 4 and without leading 0x an uppercase letters
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()

command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)
time.sleep(.5)
Introllingcode=Introllingcode+1 # increment rollingCode for the next command to sen
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()
command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)
ser.close()
print("Executet:  <UP> on " + address + " with last Rolling Code " + STRRollingCode)

Here you have to replace the Address and Baudrate by the values you choose before. To trigger the script I use another button, defined in the same item file used before:

Switch MyShutterButton "Shutter moving" <switch> #Ofcourse you can reuse the first button, by just changing the rule

And for this item I defined a rule in the rule file before:

rule "Shutter UP"
    when
	Item MyShutterButton received command ON
    then
    val result =executeCommandLine(Duration.ofSeconds(2),"python", "/etc/openhab/scripts/OpenAllShutter.py", (rollingNumber_Arbeitszimmer.state as DecimalType).intValue.toString)
    rollingNumber_Arbeitszimmer.postUpdate(2 + (rollingNumber_Arbeitszimmer.state as DecimalType))
    logInfo("Python-Cul",result)
end

The rollingCode is incremented in the python script once, but I have to update the number item, too. This is done with the second line. This time by 2 because, I send to commands.

Moving the shutters down I use the following script:

#!/usr/bin/python
import serial
import argparse
import time

parser = argparse.ArgumentParser(description="Sends SOMFY RTS commands with the nanoCUL on the selected port")
parser.add_argument("rollingCode", help="The rollingCode", type=str)
args = parser.parse_args()

#Fixe Adresse fuer alle Shutter
address='00000A'
#Encryptionkey Somfy
encryptionKey = 'A1'

# Baudrate used by Somfy
BaudRate = 38400  #In my case 38400 but I found 9600, too. But it was not working so it is just try and error

#Schnittstelle zum CUL
ser = serial.Serial('/dev/ttyUSB0', BaudRate, timeout=.1)

#Parse RoolingCode to integer
Introllingcode=int(args.rollingCode)

# Command (1 = My, 2 = Up, 4 = Down, 8 = Prog)
C="4"

#Parse integer RollingCode to HEX String with lengh 4 and without leading 0x an uppercase letters
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()

command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)
time.sleep(.5)
Introllingcode=Introllingcode+1 #1
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()
command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)

time.sleep(7)


C="1"
Introllingcode=Introllingcode+1 #2
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()
command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)

time.sleep(.5)

C="2"
Introllingcode=Introllingcode+1 #3
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()
command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)
time.sleep(.5)
Introllingcode=Introllingcode+1 #4
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()
command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)

time.sleep(2)

C="1"
Introllingcode=Introllingcode+1 #5
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()
command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)

time.sleep(.5)

C="4"
Introllingcode=Introllingcode+1 #6
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()
command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)
time.sleep(.5)
Introllingcode=Introllingcode+1 #7
STRRollingCode=hex(Introllingcode)[2:].zfill(4).upper()
command = 'Ys' + encryptionKey + C + '0' + STRRollingCode + address + "\n"
ser.write(command)
ser.close()
print("Executed:  <DOWN> on " + address + " with last Rolling Code " + STRRollingCode)

I use this script to completely close my shutters and change the angle of the blinds to close. I move up some time to make the script work, even if the shutter already moved to the lowest position. Than I move down, up and down again. his way my blinds change their angle to close.

The script is triggered by the rule:

rule "Shutter Down"
    when
	Item MyShutterButton received command OFF
    then
    val result =executeCommandLine(Duration.ofSeconds(30),"python", "/etc/openhab/scripts/CloseAllShutter.py", (rollingNumber_Arbeitszimmer.state as DecimalType).intValue.toString)
    rollingNumber_Arbeitszimmer.postUpdate(8 + (rollingNumber_Arbeitszimmer.state as DecimalType))
    logInfo("Python-Cul",result)
end

Since I send so many commands, I have to update the RollingCode by 8.

Final remark: To enable persistence:
Final Remark II: With the last update, the following lines were not working anymore. It seems that Openhab integrated/activated an standard service for persisted items. I had to remove the file rrd4j.persist.
I installed the rrd4j feature via Paper UI. I created the file rrd4j.persist under /etc/openhab/persistence. Here the name of the file is important. The content is:
```
Strategies {

  • default = everyChange*
    }

Items {

  • CULRollingNumber : strategy = restoreOnStartup*
    }
    ```
    This will work, if the group of the number items is CULRollingNumber, a I defined it in my item file.

I hope this helps someone, to get started.

Hi Oliver the item definition is:

Number rollingNumber_AllShutter "Befehlcounter alle Shutter" <CULRollingNumber> (RollerCommandCounter)

I updated my rules to:

val result =executeCommandLine(Duration.ofSeconds(2),"python", "/etc/openhab/scripts/OpenAllShutter.py", (rollingNumber_AllShutter.state as DecimalType).intValue.toString)

This way the number ist send as Integer.

Hey Christoph,
many thanks for your tutorial.
Sending your command twice to your somfy motor is annoying and sounds more to me that there is some break of communication?
Chacka mentions in his post above, that the transmitter resets if you start serial communication. did you try to wait for 2 seconds before sending a command?

Based on your script I am going to expand it to my needs and will post the results here.
I am going to do the following additions:

  • make the script independent from item names so that it can be used for all RTS motors (usage of groups)
  • add calibration so that you can send commands like 60% to your device

Hi Oliver,

yes your are right. I forgot this in my tutorial, but it was already implemented. I added it in the tutoriel. I don’t think that the problem is on the raspberry side. I observed the same behavior with the Binding from Daniel_Weisser running on OpenHab 2.5. Maybe it is just a different/old version of motors installed at my place.

Of course it would be nice to have only one script and call it with parameters. Te point is, that I need to take care of the rollingCode. Yo can have the selection of the Address in the script or at the switch/rule side. Since I was trying a lot and always lost the rollingCode. I ended up in having one file for one Address. I taught one Address to all my shutters and a different to the single shutter. This way I only have to send the command once. This way their are not to many tasks for the CUL to run in parallel.

How do you manage the 60%. You could stop the time, needed going down to 60%. But what will happen, if you start completely closed, or at an arbitrary position. Do you go up and then down?

The rolling code will be easily pulled from a persisted item. There is a structure required for all 3 items which makes it easy to get the rolling code with a single line for all items:

  • <itemName>_control (item which receives commands in any NUMBER format. 0 = up, 100 = down, e.g. 66 = move awning down 66%)
  • <itemName>_rollingCode (persisted item, which stores the last rollingCode as STRING)
  • <itemName>_calibration (persisted item, which stores the duration of a half cycle from 0% to 100% in seconds as NUMBER. Required to control the motor with a command like 60%)

Exactly.I do not put too many effort in this - if awning is not at 0% you better stop by hand. Makes only sense if you want to move it from 0% position to a non-100% position.

I am currently writing a script but do not have somfy hardware here:
If I have to stop a moving device I hit the same button again, right?
What happens if I press the other button while the device is moving?

To Stop a device, you have to send “MY” equals “1” If you send another code the currently running movement will be stopped and the new one one will be executed.

I see. And if you press MY when the motor is not moving then it starts moving to the predefined MY postion?

Yes it should behave like this.

Hi Christoph,

thank you very much for this much needed tutorial. I got the CUL Switch for programming the motors on my sitemap and also a Switch Item for the rollershutters. I configured the real Item for the Shutters as “Rollershutter item” because I kind of missed the part where you configured the actual rollershutters as actual item. You justed wrote in the rules

when
	** Item AllShutter** received command OFF
    then

but in the tutorial you never defined the item for the rollershutters.

I think that the scripts are working (started from the command line, but I cannot get the rollingCode). However, the items on my sitemap seem to have no connection. I don’t know if it didn’t get the rule or something else…
Is there a way to find out the rollingCode?

Another question: Could you show me the definition of your rollershutter item? Maybe I have the wrong item type or something… I am not quite sure… I tried some out, but they did not work.
For debugging reasons I put the rollingCode on my sitemap, but as string and also as number he only shows an “-”.

Anyway, I will keep you updated! Thank you again for the scripts and configuration.

Hi Floh,

you have not missed anything. I made a mistake. I forgot to implement the second Switch, after programming the new Address. I corrected my tutorial. So there is no Rollershutter item. The reason is, that I would not use it.
There are two points:

  1. My Shutter behave very strange: If I send the command once. They only move a small step.
  2. My shutters are completely opened or close.

This is why I do not need a Rollershutter item. But you can easily adjust my rule. Just replace the Line

rule "Shutter UP"
    when
	Item MyShutterButton received command ON
    then

with

rule "Shutter UP"
    when
	Item <Your Shutter Item> received command UP
    then

and the same for DOWN.

Best regards,
Christoph

EDIT: changes to the description and the script to reflect all feedback until 21.04.2021

As announced this is a rule for sending commands to Somfy RTS devices such as rollershutters, awnings, etc.
create 4 items as described in the rule and follow the instructions.

.rules file

import org.openhab.core.model.script.ScriptServiceUtil
/*
=======================================================================================================================
DESCRIPTION:
  1. Create a group with a generic group name like `gRTS`. Note: this is not a group which represents a physical device/thing, it is a logical group.
     If you choose a different group name, make sure to change the rule accordingly.
  2. Add the following item for each device with the following syntax:
     <itemName>_control			(Type: STRING; item which sends commands to this rule. Valid commands: 0 or UP, STOP or MY, 100 or DOWN, PROG, NUMBER between 1 and 99)
     <itemName>_rollingCode		(Type: NUMBER; persisted item, which stores the last rollingCode)
     <itemName>_calibration		(Type: NUMBER; persisted item, which stores the duration of a half cycle from 0% to 100% in seconds. Required to control the motor with a command like 60[%])
     <itemName>_address			(Type: STRING, 6 digit; persisted item, which stores address of device)
  3. Add all items with suffix `_control` to your group. Do NOT add other items to that group.
  4. - specify the name and location of your python script
     - items for Rolling Code, Calibration and Address may not be NULL (use REST API to set their values) and have to be persisted.
     - If there are any problems with this script, set debug variable in this rule to true
=======================================================================================================================

=======================================================================================================================
CHANGE VALUES ACCORDINGLY: */

var Boolean debug = false
val String fileName = "RTS.rules"
val String scriptCommand = "python3"
val String scriptName = "/etc/openhab/scripts/RTS.py"
rule "RTS"
//======================================================================================================================

when
    Member of gRTS received command
then
    if (debug) logInfo(fileName,"Rule started")
    var String strThing = triggeringItem.name.toString().split("_").get(0)
    var String strItem = triggeringItem.name.toString()
    var String strCommand = receivedCommand.toString()
        if (debug) logInfo(fileName, "Thing: '{}', Item: '{}', Command: '{}'",strThing,strItem,strCommand)
    var String strRTSCode = ""
    var Integer intCommand = 0
    var Float fSeconds = 0.0

//find regular commands
    switch strCommand.toUpperCase {
        case "" : {
            logInfo(fileName,"Item: '{}' received empty command",strItem)
            return
        }
        case "MY", case "STOP" : strRTSCode = "1"
        case "UP" : strRTSCode = "2"
        case "DOWN" : strRTSCode = "4"
        case "PROG" : strRTSCode = "8"
//find number values in command
        default: {
	        if (debug) logInfo(fileName,"No standard commands submitted")
            var strCommand2 = transform("REGEX", "\\D*(\\d+)\\D*", strCommand)
                if (debug) logInfo(fileName,"strCommand2: '{}'",strCommand2)
            if (strCommand2 !== null) {
                intCommand = Integer.parseInt(strCommand2)
                    if (debug) logInfo(fileName,"intCommand: '{}'",intCommand.toString())
                switch intCommand {
                    case 0 : strRTSCode = "2"
                    case 100 : strRTSCode = "4"
                    case intCommand > 100 : logInfo(fileName,"Item: '{}' received command '{}': not in range [0..100]",strItem,intCommand.toString())
//calculate time when motor has to stop
                    default: {
                        strRTSCode = "4"
                        var Float fCalibration = (ScriptServiceUtil.getItemRegistry.getItem(strThing+"_calibration").state as DecimalType).floatValue
		            if (debug) logInfo(fileName, "fCalibration: '{}'",fCalibration.toString())
                        fSeconds = intCommand.floatValue * fCalibration / 100
                            if (debug) logInfo(fileName, "fSeconds: '{}'",fSeconds.toString())
                    }
                }
            } else {
                logInfo(fileName,"Item: '{}' received unknown command '{}'",strItem,strCommand)
                return
            }
        }
    }

//prepare serial data payload for python script
//Syntax: Ys + EncryptionKey=A1 + Command + 0 + RollingCode + Address
    var Integer intRollingCode = (ScriptServiceUtil.getItemRegistry.getItem(strThing + "_rollingCode").state as DecimalType).intValue
        if (debug) logInfo(fileName, "intRollingCode: '{}'",intRollingCode.toString())
    var String strRollingCodeHex = String::format("%04X", intRollingCode)
    var String strAddress = ScriptServiceUtil.getItemRegistry.getItem(strThing + "_address").state.toString()
        if (debug) logInfo(fileName, "PAYLOAD: strRTSCode: '{}', strRollingCodeHex: '{}', strAddress: '{}'",strRTSCode,strRollingCodeHex,strAddress)
    var String strPayLoad = "YsA1" + strRTSCode + "0" + strRollingCodeHex + strAddress + "\n"
        if (debug) logInfo(fileName, "PAYLOAD STRING: '{}'",strPayLoad)
    var result = executeCommandLine(Duration.ofSeconds(2),scriptCommand, scriptName, strPayLoad)
        logInfo(fileName, "Python script result: {}",result)
    ScriptServiceUtil.getItemRegistry.getItem(strThing + "_rollingCode").postUpdate(intRollingCode + 1)

//send STOP after fSeconds if command is number from 1 to 99
    if (fSeconds > 0.0) {
        strRTSCode = "1"
        strRollingCodeHex = String::format("%04X", intRollingCode +1)
        strPayLoad = "YsA1" + strRTSCode + "0" + strRollingCodeHex + strAddress + "\n"
            if (debug) logInfo(fileName, "PAYLOAD STRING timer triggered: '{}'",strPayLoad)
        createTimer(now.plusSeconds(fSeconds.longValue), [ |
            var result = executeCommandLine(Duration.ofSeconds(2),scriptCommand, scriptName, strPayLoad)
                logInfo(fileName, "Python script result timer triggered: {}",result)
            ScriptServiceUtil.getItemRegistry.getItem(strThing + "_rollingCode").postUpdate(intRollingCode + 2)
        ])
    }
end

Python script (copy to /etc/openhab/scripts/)

#!/usr/bin/python3
# cudos go to chacha
# Install pyserial from https://pythonhosted.org/pyserial/pyserial.html#installation
# add 'stty -F /dev/ttyUSB0 -hupcl' to file /etc/rc.local
import serial
import sys

payLoad = sys.argv[1]
print("Python script received payload " + payLoad)
ser = serial.Serial('/dev/ttyUSB0', 38400, timeout=.1)
	#or: ser = serial.Serial('/dev/ttyUSB0', 9600, timeout=1)
ser.write(payLoad.encode)
print ser.readline()
	#todo: throw error if not ok
ser.close()

Reason for editing: adding automatic stop to rule