Soma Blinds - Native support with Bluetooth Binding

I recently bought some blind controllers from Soma. While this is not a review, I will say they work OK. I am planning on buying 5 more.

I first for them working a rather lumpy way with a python script and http server. Details here This worked but was unreliable do to my use of lamdas. Details here
This worked and could have been rewritten a number of ways to work better but it was just not a solid solution.

With lots of help from @vkolotov Vlad and his binding I managed to get the gatt files so now his BlueTooth binding will do all the hard work for us.:smiley:

To set it up -

  1. Follow the tutorial for Vlad’s binding
  2. Copy the gatt files for soma into your local directory

Service:

Characteristic

  1. Once the binding has been added it should detect the blind controllers. Mine showed up as “S”. Nothing much you can do there.

  2. Add them as a thing. Give them a decent name.

  3. Create a blinds.items file and add the following

Number FrontRoomBlindsRight_Battery             "Front room blinds - Right - Battery [%.0f %]"     {channel="bluetooth:ble:C1037491056C:180F-2A19-level"}
Switch FrontRoomBlindsRight_Connected           "Front room blinds - Right - Connected"            {channel="bluetooth:ble:C1037491056C:connected"}
Switch FrontRoomBlindsRight_Online              "Front room blinds - Right - Online"               {channel="bluetooth:ble:C1037491056C:online"}
Number FrontRoomBlindsRight_Position            "Front room blinds - Right - Position [%.0f %]"    {channel="bluetooth:ble:C1037491056C:1861-1525-position"}
Number FrontRoomBlindsRight_Target              "Front room blinds - Right - Target"               {channel="bluetooth:ble:C1037491056C:1861-1526-target"}
Number FrontRoomBlindsRight_MotorControl        "Front room blinds - Right - Motor"                {channel="bluetooth:ble:C1037491056C:1861-1530-target"}
Number FrontRoomBlindsRight_RSSI        "Front room blinds - Right - RSSI"                         {channel="bluetooth:ble:C1037491056C:rssi"}

Note that your mac address will obviously be different.

All the items are pretty self explanatory with the exception of MotorControl. I need to improve it slightly to accept UP and DOWN but for now you can issue a 105 or 150 to move it up or down.
I’ll update this as and when I make changes.

Hopefully someone else using Soma blinds can find this useful.

4 Likes

Well done @CDriver! This can be useful not only for Soma blinds owners, but also as a guideline for other users who want to add support for some other cool BT devices. In this case, there was not any code changes (just fairly simple XML files), so it should have been quite easy (I hope :sunglasses: ) to implement :wink:

Hey @CDriver,

and thanks a lot for your work! I also got myself some Soma controllers and I am trying to set them up right now. So far everything worked, but can you assist me with the xml files? Where do I find them and where do I put them?

Thanks!

EDIT: Found the folder, but I think I need some more files. Currently I just have the ones from your linked post (org.bluetooth.characteristic.blind_position.xml + org.bluetooth.characteristic.blind_position.xml )

EDIT2: Found it, too :slight_smile: Seems like it is not yet merged in the version I downloaded today. Is this the most current and working version? https://github.com/sputnikdev/bluetooth-gatt-parser/pull/9

EDIT3: Something seems not working. Can you make something out of this error?
Exception while formatting undefined value [sourcePattern=%.0f %, targetPattern=%1$s %, exceptionMessage=Conversion = '%']

Unless @vkolotov has had some time to add the new functionality, this does not work as it should.

It disconnects from the device and does not mange to reconnect. I’ve had confirmation from the manufacturer that this is by-design. They don’t support continuous connections.

I ended up reverting to use the http method.


and also

I have the latter running on a hidden pi near the curtains. This is quite far from my OH RPi which was struggling with connection issues.

Thanks, man! I would not have figured this out on my own. After struggling a little, here are my findings to get the whole thing reproduced:

For openhab I only needed the webshades library ( https://github.com/paolotremadio/SOMA-Smart-Shades-HTTP-API/blob/master/webshades.py ). However, since openhab and webshades run both on port 8080, a small modification was needed. In webshades.py I added:

import os

os.environ["PORT"] = "8012"`

The item definition was a little complicated, since you need to escape a “:” with its HTML-code, which also needs to be escaped (":" -> “%%3A”). I implemented this using the http1-binding:

String BlindsLeft_Battery      "Rollo Links - Batterie [%d %%]"     <battery>  (gBlinds)                      { http="<[http://openhabianpi:8012/getbattery/CE%%3A6F%%3A3B%%3AB3%%3A3E%%3A62:240000:REGEX((.*))]"}
Dimmer BlindsLeft_Position     "Rollo Links [%d]"                   <blinds>   (gBlinds)   ["Lighting"]	   { http=">[CHANGED:GET:http://openhabianpi:8012/setposition/CE%%3A6F%%3A3B%%3AB3%%3A3E%%3A62/%2$s]"}

Since my Soma is quite slow in responding, I also needed to increase the timeout significantly in services/http.cfg:

# timeout in milliseconds for the http requests (optional, defaults to 5000)
timeout=50000

Thanks for posting an alternative solution.

How are you controlling them? Basic UI or habpanel?

A note: if you have multiple blinds your method could have some concurrency problems. Two blinds in a group, if you send request to the group the items fire almost at the same time. This will cause weird behaviour with the Bluetooth connection. It did on mine anyway.

Just a heads up.

Basic UI, which works just fine. I am also trying to use them with Alexa, but something is not working. I have not completely figured it out, but so far I think it has to do with the “open” and “closed” states. When I give Alexa a percentage value, it almost always seems to work.

I did not yet run into the concurrency problem, but I’ll have a look at it.

However, the webshades webserver seems to be not responding after some time. Does that happen in your setup too? My quick fix would be to simply restart it sometime in the night; but maybe someone else figured out a better explanation/solution?

I recently got alexa working just fine and I wanted to share my implementation in case anyone is reading:

rule "Right Blind"
when
    Item ALBlindsRight_Position received command
then
    val String device_mac_addr = "C7:23:9A:xx:xx:xx"

    var Number pos = 0
    if (receivedCommand == STOP) {
        logWarn(filename, "Soma-Blinds | STOP is not yet supported. Aborting.")
        //Workaround with get_position does not work either, because the device returns instantly the target position and not the current position while moving.
    } else if (receivedCommand == ON || receivedCommand == UP) {
        pos = 0
    } else if (receivedCommand == OFF || receivedCommand == DOWN) {
        pos = 100
    } else {
        try {
            pos = receivedCommand as Number
        } catch (Exception e) { // String is not a number
            logError(filename, "Soma-Blinds | The command '" + receivedCommand + "' is not a number or a known value. Aborting.")
            return;
        }
    }

    val String cmd = "sudo /usr/bin/python /etc/openhab2/exec-scripts/Homebridge-SOMA-Smart-Shades/control.py -t "+ device_mac_addr +" -c move_target --motor_target " + pos
    if (controllerIsLocked) {
        logWarn(filename, "(Debug)Soma-Blinds | Controller was locked for "+triggeringItem.name+", retrying in "+cmdPause+" milliseconds.")
        createTimer(now.plusMillis(cmdPause)) [ |
            sendCommand(triggeringItem.name, receivedCommand.toString)
        ]
    } else {
        controllerIsLocked = true;

        currentAttempt++
        var String errorMsg = ""
        logDebug(filename, "executing "+currentAttempt+"/"+maxAttempts+": " + cmd)
        val String status_raw = executeCommandLine(cmd, cmdTimeout)
        if (status_raw === null) {
            errorMsg = "Status was null."
        } else {
            val String statusWithoutLinebreaks = status_raw.replaceAll("\\r?\\n|\\r","")
            if (status_raw.length == 0) {
                errorMsg = "Response was empty."
            } else if ( //filtering known errors
                status_raw.contains("function not implemented") ||
                status_raw.contains("no route to host") || 
                status_raw.contains("discover all characteristics failed") || 
                status_raw.contains("discover all primary services failed") || 
                status_raw.contains("software caused connection abort") 
            ) {
                errorMsg = statusWithoutLinebreaks
                logError(filename, "Soma-Blinds | New kind of error detected: '" + errorMsg)
            } else {
                logInfo(filename, "Soma-Blinds | Success by executing try #" + currentAttemptLeft + " @"+triggeringItem.name+". Output for '"+cmd+"' is: '" + statusWithoutLinebreaks + "'.")
            }        }
        if (errorMsg == "") { //success
            currentAttempt = 0
            logDebug(filename, "Soma-Blinds | Position has been set after "+currentAttempt+" attempts by executing '"+cmd+"'.")
        } else { //retry
            if (currentAttempt <= maxAttempts) {
                logWarn(filename, "Soma-Blinds | Error in setting position in attempt #"+currentAttempt + ". Executing "+cmd+" caused: '" + errorMsg +"'.")
                createTimer(now.plusMillis(cmdPause)) [ |
                    sendCommand(triggeringItem.name, receivedCommand.toString)
                ]
            } else {
                logError(filename, "Soma-Blinds | Not able to set position in "+maxAttempts+" attempts by executing '"+cmd+"'.")
            }
        }
        controllerIsLocked = false;
    }
end

//My room has two windows with seperate blinds. This rule helps in handling both as one, while still having the ability to use each one seperate.
rule "Blinds Meta Controll"
when
    Item ALBlinds_Position received command
    or Item ALBlinds_Position_Alt received command
then
    //0 = up
    //100=down
    ALBlindsLeft_Position.sendCommand(receivedCommand.toString)
    Thread::sleep(cmdPause) //todo fiddle with this
    ALBlindsRight_Position.sendCommand(receivedCommand.toString)
    if (triggeringItem.name === "ALBlinds_Position") {
        ALBlinds_Position_Alt.postUpdate(receivedCommand.toString)
    } else {
        ALBlinds_Position.postUpdate(receivedCommand.toString)
    }
end

rule "Rollo Batteriestatus" 
when
    Time cron "0 11 4,11,18 * * ? *" //4,12,19 Uhr
then
    var String device_mac_addr = "CE:6F:3B:xx:xx:xx"
    var String device_name = "ALBlindsLeft_Battery"
    controllerIsLocked = true;
    var String cmd = "sudo /usr/bin/python /etc/openhab2/exec-scripts/Homebridge-SOMA-Smart-Shades/control.py -t "+ device_mac_addr +" -c get_battery"
    var int attemptNumber = 1
    var String errorMsg = null
    var String batteryValueAsString = null
    while (attemptNumber < 10 && batteryValueAsString === null) {
        val status_raw = executeCommandLine(cmd, cmdTimeout)
        val statusWithoutLinebreaks = status_raw.replaceAll("\\r?\\n|\\r","")
        val positionOfValue = statusWithoutLinebreaks.indexOf("get_battery ")
        if (positionOfValue > 0) { //success
            batteryValueAsString = statusWithoutLinebreaks.substring(positionOfValue+12).trim()
            //val Number batteryValue = Integer::parseInt(batteryValueAsString)
            sendCommand(device_name,batteryValueAsString)
            attemptNumber = 0
        } else if (status_raw.length == 0) {
            errorMsg = "Response was empty."
        } else if (status_raw.contains("function not implemented") ) { //occurs randomly
            errorMsg = statusWithoutLinebreaks
        } else {
            errorMsg = "Unknown error: " + status_raw
        }
        if (batteryValueAsString === null) {
            logWarn(filename, "Soma-Blinds | Error in getting battery state for "+device_name+" in attempt #"+attemptNumber + ". " + errorMsg)
            attemptNumber = attemptNumber+1
        }
    }
    

    //right
    device_mac_addr = "C7:23:9A:xx:xx:xx"
    device_name = "ALBlindsRight_Battery"

    cmd = "sudo /usr/bin/python /etc/openhab2/exec-scripts/Homebridge-SOMA-Smart-Shades/control.py -t "+ device_mac_addr +" -c get_battery"
    attemptNumber = 1
    errorMsg = null
    batteryValueAsString = null
    while (attemptNumber < 10 && batteryValueAsString === null) {
        val status_raw = executeCommandLine(cmd, cmdTimeout)
        val statusWithoutLinebreaks = status_raw.replaceAll("\\r?\\n|\\r","")
        val positionOfValue = statusWithoutLinebreaks.indexOf("get_battery ")
        if (positionOfValue > 0) { //success
            batteryValueAsString = statusWithoutLinebreaks.substring(positionOfValue+12).trim()
            //val Number batteryValue = Integer::parseInt(batteryValueAsString)
            sendCommand(device_name,batteryValueAsString)
            attemptNumber = 0
        } else if (status_raw.length == 0) {
            errorMsg = "Response was empty."
        } else if (status_raw.contains("function not implemented") ) { //occurs randomly
            errorMsg = "random error: " + statusWithoutLinebreaks
        } else {
            errorMsg = "Unknown error: " + status_raw
        }
        if (batteryValueAsString === null) {
            logWarn(filename, "Soma-Blinds | Error in getting battery state for "+device_name+" in attempt #"+attemptNumber + ". " + errorMsg)
            attemptNumber = attemptNumber+1
        }
    }
    controllerIsLocked = false;
end

and this is my item definition

//blinds.items
Group    gBlinds    "Rollos"    <blinds>

Rollershutter   ALBlindsLeft_Position             "Rollo Links André [%d]"                                              <blinds>    (gBlinds)
Number          ALBlindsLeft_Battery              "Akku Rollo Links André [%d %%]"                                      <battery>   (gBlinds, gBattery, gMonitorUpdates)

Rollershutter   ALBlindsRight_Position            "Rollo Rechts André [%d]"                                             <blinds>    (gBlinds)
Number          ALBlindsRight_Battery             "Akku Rollo Rechts André [%d %%]"                                     <battery>   (gBlinds, gBattery, gMonitorUpdates)

//meta item for alexa and the likes
Rollershutter   ALBlinds_Position                 "Rollo / Rollos [%d%% gesenkt]"                                       <blinds>    {homekit="WindowCovering", alexa="RangeController.rangeValue" [category="INTERIOR_BLIND", friendlyNames="@Setting.Opening", unitOfMeasure="Percent", actionMappings="Close=100,Open=0,Lower=(+25),Raise=(-25)", stateMappings="Open=0:25,Closed=26:100"] }

Sometimes it takes a moment because of the timeouts and the concurrency problems… but I am quite happy with it. However, I appreciate any improvements to my code :slight_smile: