Sensibo API Integration - Based on Rules

For those with a Sensibo system, I’ve posted the files that I’m using successfully to control my system on Github. If you have any questions, corrections, or suggestions, please let me know.

2 Likes

Hi Michael,

I come across your code for OH2 and Sensibo. Does this work with Sensibo Sky or is it meant for the Sensibo Bridge and Pods version?

I’ve tried the URLs from the sensibo-interface.rules file manually with the PODID and APIKEY. They give me the values as expected. Based on that Sensibo Sky looks like supported, but how to integrate in OH2…

I’m new to OpenHAB, installed it yesterday and tried to add your item, sitemap and rule. I see the sitemap in Basic UI, but have no idea how to add Sensibo as a Thing. Can you briefly describe? (couldn’t find that on your GitHub)

Thanks!

I use it with the bridge and pod version, but it’s good to hear that it works with the new version as well. I looked at the Sensibo API and it doesn’t look like they’ve changed it as a result of the new hardware.

You don’t need to create a thing in OH2, just the items and rules. When the items are changed (via the UI or otherwise, such as with the REST API), the rules will be triggered and reach out to the Sensibo API. I realize that this is not how OH2 is supposed to work, but unfortunately I don’t know nearly enough about Java to create a proper Sensibo binding.

I would recommend testing your setup with the REST API, since it lets you interact with the items directly.

I should also note that the SensiboState switch item can be used with Homekit/Siri and Alexa very easily by adding a [“Switchable”] tag, which allows you to control your Sensibo with voice commands. Personally, I’ve found this very useful. [Edit: I’ve added this tag to the .items file and made a note of this feature on Github.]

I hope that helps, but let me know if you have any trouble.

Hi Michael,

Thanks for getting back to me. You wrote you don’t need to do anything else than the items and rules. But how do I get (discover) the “Thing” in OH2? I don’t see it anywhere.

I don’t have voice yet configured, but will definitely try the Switchable tag.

Will let you know once I’ve it running.

Regards,
Ferry

Things require a binding, which we’re not using in this case.

However, you can see the items in the Paper UI at http://your-server:8080/ui/index.html#/configuration/items. As long as they’re listed there, you can interact with the items via a GUI (I use the Basic UI and the sitemap I provided), or the REST API. And as I mentioned above, changing the item states via a GUI or the REST API (or via a rule, etc.) will trigger the rules to send commands to the Sensibo API.

It sounds like your setup should be all set, but let me know if you have any trouble.

@Ferry, just to close the loop, were you able to get things working the way you like?

Hi Michael,

Unfortunately not. Currently on travel, so little time to dive into it.
Will need to understand the OpenHab2 configuration file structures and convert it from your OpenHab provided configs.

The files I’ve provided are for OpenHAB 2; maybe I should make a better note of that. If you simply copy them into their respective folders and add your API key and pod ID to the rules file, it should work.

Hi Michael,

Had some time to spent on OH2 and Sensibo. Status = not running yet, but progressing.
As we noticed, we have a difference in Sensibo equipment. You’re running with bridge, where I’m using Sensibo Sky. When I try the API’s manually they seem to be the same and all do work.

For now I’m able to see the Sensibo in my Sitemap. Fire commands to it from Sitemap and my airco reacts on them. I’m having issue with the temperature and humidity reading. OpenHAB2 logging shows me the sensibo-interface.rules contains some issues.

[ERROR] [del.script.Sensibo read measurements] - Error was caught: java.lang.NumberFormatException: null

I’ve seen that the information is retrieved, but the code has a NumberFormatException in the “read measurements” section. Unfortunately I’m not a programmer and share it with you to find a solution. Will Google as well to find a fix in parallel.

I will share the steps taken once it is running.

Ferry

Hi Michael,

I found the issue, see the line below which you get back from Sensibo.

Error was caught: java.lang.NumberFormatException: For input string: "{“status”: “success”, “result”: [{“batteryVoltage”: null, “temperature”: 23.2, “humidity”: 52.4}]}”

The Sensibo Sky doesn’t have a battery so batteryVoltage:null

Removed it from the configuration and see my SiteMap part. Here I should now delete the Battery information.

Next things is testing it, documenting it for sharing and see if I can create some automation.

Greets,
Ferry

1 Like

Ah, nice catch! That makes sense.

You’re right though, it will still throw an error if no result is returned from the Sensibo API. I’m adding in and testing some error handling at the moment to see if I can correct that.

Update: Okay, I’ve added error handling for reading from the API, and separated the battery reading into a separate rule for easy removal. I also corrected a syntax error that I had missed. Let me know if this works better for you.

I’ve pushed another update to make writes to the pods more reliable by adding a timeout and automatic retry function. I sometimes experience commands getting dropped, and this should catch these and correct them.

As always, more feedback is more than welcome. Now that other folks are actually using my code, it’s gotten a lot better in a short period of time. Thanks!

Hi Michael,

I’ve been tweaking the Sensibo Sky OH2 config files to include API supported Swing function. That works just fine on my AC. Selected only four modes, so it fits nicely on my phone screen.

Next to that I changed the Target icon to heating so it gets a dynamic colored knob. The same for the Humidity icon, changed it to hunidity and gives a colored gauge.

.items

Switch   SensiboState    ["Switchable"]
Number   SensiboTemp
Number   SensiboHumidity
Number   SensiboTarget
String   SensiboFan
String   SensiboMode
String   SensiboSwing

.sitemap

    Switch    item=SensiboState      label="On/Off"                   icon="switch"
    Switch    item=SensiboMode       label="Mode"                     icon="heating"        mappings=[cool="Cool",heat="Heat",fan="Fan"]
    Setpoint  item=SensiboTarget     label="Target [%.0f ºC]"         icon="heating"        minValue=18 maxValue=26 step=1.0
    Switch    item=SensiboFan        label="Fan"                      icon="fan"            mappings=[low="Low",medium="Med",high="High",auto="Auto"]
    Switch    item=SensiboSwing      label="Swing"                    icon="fan"            mappings=[stopped="Stop",fixedMiddle="Middle",fixedTop="High",rangeFull="Swing"]
    Text      item=SensiboTemp       label="Temperature [%.1f ºC]"    icon="temperature"
    Text      item=SensiboHumidity   label="Humidity [%.0f %%]"       icon="humidity"

.rules

val String APIKey = "API-key"
val String PodID = "code"
var Boolean PodBoolean
/*** Read Sensibo State ***/
rule "Read Sensibo State"
  when
    System started
  then
  try {
    var String PodStatus = executeCommandLine('curl -sSH "Accept: application/json"     "https://home.sensibo.com/api/v2/pods/' + PodID + '/acStates?apiKey=' + APIKey + '&limit=1&fields=acState"', 5000)
  if (PodStatus.contains('"status": "success"')) {
    val String PodOn = (transform("JSONPATH", "$.result[0].acState.on", PodStatus))
    val String PodMode = (transform("JSONPATH", "$.result[0].acState.mode", PodStatus))
    val Number PodTarget = new Integer(transform("JSONPATH", "$.result[0].acState.targetTemperature", PodStatus))
    val String PodFan = (transform("JSONPATH", "$.result[0].acState.fanLevel", PodStatus))
    val String PodSwing = (transform("JSONPATH", "$.result[0].acState.swing", PodStatus))
    if (PodOn == "true")
      {   postUpdate(SensiboState, ON)   }
    if (PodOn == "false")
      {   postUpdate(SensiboState, OFF)  }
    postUpdate(SensiboTarget, PodTarget)
    postUpdate(SensiboMode, PodMode)
    postUpdate(SensiboFan, PodFan)
    postUpdate(SensiboSwing, PodSwing)
  }
  else {
      logError("Pod state", "Error reading from Sensibo API")
    }
  }
  catch(Throwable t) {
    logError("Sensibo read state", "Error was caught: {}", t)
  }
end
/*** Read Sensibo Temperature and Humidity ***/
rule "Read Sensibo Temp and Humidity"
  when
    System started or
    Time cron "0 0/5 * * * ?"
  then
try {
    var String PodMeasurements = executeCommandLine('curl -sSH "Accept: application/json"     "https://home.sensibo.com/api/v2/pods/' + PodID + '/measurements?apiKey=' + APIKey + '&fields=temperature,humidity"', 5000)
    if (PodMeasurements.contains('"status": "success"')) {
      val Number PodTemperature = new Double(transform("JSONPATH", "$.result[0].temperature", PodMeasurements))
      val Number PodHumidity = new Double(transform("JSONPATH", "$.result[0].humidity", PodMeasurements))
      postUpdate(SensiboTemp, PodTemperature)
      postUpdate(SensiboHumidity, PodHumidity)
    }
    else {
      logError("Pod temp and humidity", "Error reading from Sensibo API")
    }
  }
  catch(Throwable e) {
    logError("Sensibo read temp and humidity", "Error was caught: {}", e)
  }
end
/*** Read Sensibo Battery (v1 Pods only) ***/
/*** rule "Read Sensibo Battery"
  when
    System started or
    Time cron "0 0 * * * ?"
  then
  try {
    var String PodMeasurements = executeCommandLine('curl -sSH "Accept: application/json"     "https://home.sensibo.com/api/v2/pods/' + PodID + '/measurements?apiKey=' + APIKey + '&fields=batteryVoltage"', 5000)
    if (PodMeasurements.contains('"status": "success"')) {
      val Number PodBattery = new Integer(transform("JSONPATH", "$.result[0].batteryVoltage", PodMeasurements))
      postUpdate(SensiboBattery, PodBattery)
    }
    else {
      logError("Pod battery", "Error reading from Sensibo API")
    }
  }
  catch(Throwable e) {
    logError("Sensibo read battery", "Error was caught: {}", e)
  }
end ***/
/*** Write Sensibo State ***/
rule "Write Sensibo State"
  when
    Item SensiboState changed or
    Item SensiboMode received command or
    Item SensiboFan received command or
    Item SensiboTarget received command or
    Item SensiboSwing received command
  then
  try {
    if (SensiboState.state == ON) {
      PodBoolean = true
    }
    else {
      PodBoolean = false
    }
    var String CommandURL = 'https://home.sensibo.com/api/v2/pods/' + PodID + '/acStates?apiKey=' + APIKey
    var String CommandState = '{"acState":{"on":' + PodBoolean + ','
    var String CommandMode = '"mode":"' + SensiboMode.state + '",'
    var String CommandFan = '"fanLevel":"' + SensiboFan.state + '",'
    var String CommandSwing = '"swing":"' + SensiboSwing.state + '",'
    var String CommandTemp = '"targetTemperature":' + (SensiboTarget.state as DecimalType).intValue + '}}'
    var String CommandData = CommandState + CommandMode + CommandFan + CommandSwing + CommandTemp
    sendHttpPostRequest(CommandURL, 'application/json', CommandData)
    logInfo("Sensibo command sent", CommandData)
  }
  catch(Throwable t) {
    logError("Sensibo write", "Error was caught: {}", t)
  }
end

The sensibo.rules file give me still two issues:

  1. Sensibo v1 section fails with a NULL exception

  2. The HTTP POST needs a -d’{blablabla}’ where it had -d {blablabla} and throws a 400 BAD request - POST data is not valid JSON message. I reverted back to the previous sendHttpPostRequest method for now.
    (tried several options for including the ’ character for the POST-data part. Succeeded, but than execcommand complained :frowning:

Would be good if we can fix above 2 issues.

Thanks, this is great. I’ve made a bunch of changes to reflect your comments:

  • I made the batteryVoltage and swing fields optional, as their presence seems to depend on both the pod generation and the model of AC being used.
  • As a result, I merged the measurements back into a single rule.
  • I also reorganized a bit and generally tidied things up.

I’m not getting a JSON error with the curl command on my system, but perhaps there might be differences between operating systems (I’m on OS X and Debian)? For troubleshooting, it would be helpful if you could post the results of this command from the log.

Thanks again for your help.

The optional swing function works perfect! Smart way of obtaining it from acStatus response.

It doesn’t work unfortunately for the batteryVoltage function, as the batteryVoltage field is part of the response.
{“status”: “success”, “result”: [{“batteryVoltage”: null, “temperature”: 22.8, “humidity”: 46.3}]}.

You get the following transform error:
11:40:00.365 [DEBUG] [ternal.JSonPathTransformationService] - about to transform '{"status": "success", "result": [{"batteryVoltage": null, "temperature": 22.5, "humidity": 47.1}]}' by the function '$.result[0].batteryVoltage'
11:40:00.371 [DEBUG] [ternal.JSonPathTransformationService] - transformation resulted in 'null'

Here we can either check if it contains NULL and skip the transform, or do the same acState test like with swing.
The acState test doesn’t contain the batteryVoltage with a Sensibo Sky. Not sure if it will be present with your Sensibo. Last solution would be better, so we can have a clean measurements request without the batteryVoltage field.

BTW. The httpPost workded, I was confused about the @ characters and removed them :blush:. That’s why.

It looks like the batteryVoltage field is only returned with the measurements, rather than the state. But it’s no problem, I did as you suggested and just flipped the condition for processing it to:

if (!PodMeasurements.contains('"batteryVoltage": null'))

I think that should work for both generations of pod.

I agree that the @@'s in the command are strange, it has something to do with the way the string gets parsed by executeCommandLine().

I can confirm this works as desired. It gives a nice logging output on which I noticed Sensibo only accept and forward changes in case the acState is true.

When acState is false is will answer per changedProperties [] that no changes where made. As soon as you turn on the AC, all pending changes will be set.

07:50:03.459 [DEBUG] [thome.io.rest.core.item.ItemResource] - Received HTTP POST request at 'items/SensiboSwing' with value 'rangeFull'.
07:50:03.465 [INFO ] [smarthome.event.ItemCommandEvent    ] - Item 'SensiboSwing' received command rangeFull
07:50:03.468 [INFO ] [smarthome.event.ItemStateEvent      ] - SensiboSwing updated to rangeFull
07:50:03.472 [DEBUG] [ntime.internal.engine.RuleEngineImpl] - Executing rule 'Write Sensibo State'
07:50:03.475 [INFO ] [marthome.event.ItemStateChangedEvent] - SensiboSwing changed from fixedMiddle to rangeFull
07:50:03.622 [DEBUG] [lipse.smarthome.io.net.exec.ExecUtil] - executed commandLine 'curl@@-sSH@@"Content-Type: application/json"@@-XPOST@@https://home.sensibo.com/api/v2/pods/POD-ID/acStates?apiKey=API-KEY@@-d@@{"acState":{"on":false,"mode":"cool","fanLevel":"auto","swing":"rangeFull","targetTemperature":21}}'
07:50:04.541 [DEBUG] [lipse.smarthome.io.net.exec.ExecUtil] - exit code '0', result '{"status": "success", "result": {"status": "Success", "reason": "UserRequest", "acState": {"on": false, "fanLevel": "auto", "temperatureUnit": "C", "targetTemperature": 22, "mode": "cool", "swing": "fixedMiddle"}, "changedProperties": [], "id": "W49wkr2Ujq", "failureReason": null}}'
07:50:04.551 [INFO ] [smarthome.model.script.Update result] - {"status": "success", "result": {"status": "Success", "reason": "UserRequest", "acState": {"on": false, "fanLevel": "auto", "temperatureUnit": "C", "targetTemperature": 22, "mode": "cool", "swing": "fixedMiddle"}, "changedProperties": [], "id": "W49wkr2Ujq", "failureReason": null}}
07:50:17.591 [DEBUG] [thome.io.rest.core.item.ItemResource] - Received HTTP POST request at 'items/SensiboState' with value 'ON'.
07:50:17.595 [INFO ] [smarthome.event.ItemCommandEvent    ] - Item 'SensiboState' received command ON
07:50:17.599 [DEBUG] [ntime.internal.engine.RuleEngineImpl] - Executing rule 'Write Sensibo State'
07:50:17.603 [DEBUG] [.sitemap.internal.SitemapEventOutput] - Sent sitemap event for widget 0200 to subscription aac8da47-2e35-4c60-be76-bdd50753d4eb.
07:50:17.605 [INFO ] [smarthome.event.ItemStateEvent      ] - SensiboState updated to ON
07:50:17.610 [INFO ] [marthome.event.ItemStateChangedEvent] - SensiboState changed from OFF to ON
07:50:17.725 [DEBUG] [lipse.smarthome.io.net.exec.ExecUtil] - executed commandLine 'curl@@-sSH@@"Content-Type: application/json"@@-XPOST@@https://home.sensibo.com/api/v2/pods/POD-ID/acStates?apiKey=API-KEY@@-d@@{"acState":{"on":true,"mode":"cool","fanLevel":"auto","swing":"rangeFull","targetTemperature":21}}'
07:50:18.581 [DEBUG] [lipse.smarthome.io.net.exec.ExecUtil] - exit code '0', result '{"status": "success", "result": {"status": "Success", "reason": "UserRequest", "acState": {"on": true, "fanLevel": "auto", "temperatureUnit": "C", "targetTemperature": 21, "mode": "cool", "swing": "rangeFull"}, "changedProperties": ["on", "targetTemperature", "swing"], "id": "yrJGNyAsCB", "failureReason": null}}'
07:50:18.583 [INFO ] [smarthome.model.script.Update result] - {"status": "success", "result": {"status": "Success", "reason": "UserRequest", "acState": {"on": true, "fanLevel": "auto", "temperatureUnit": "C", "targetTemperature": 21, "mode": "cool", "swing": "rangeFull"}, "changedProperties": ["on", "targetTemperature", "swing"], "id": "yrJGNyAsCB", "failureReason": null}}

Looks good to me and AFAIK there are currently no additional API options available.
Next thing would be to create a rule to automate the turn On/Off based on measured outside and inside temperature, in combination with factors if someone is at home or not for example.

Excellent, I’m glad we were able to make this better. Thanks again for your help.

I maintain my behaviour rules in a separate sensibo-logic.rules file. I use them to automate the system’s behaviour based on the indoor and outdoor temperatures, the position of the sun, and the weather forecast. They also handle basic scheduling.

Here’s my current file for reference:

/*** Max inside > 25ºC --> Sensibo Cooling ***/

rule "Max Temp > 25ºC"

  when 
    Item MaxTemp changed
  then

  if (((MaxTemp.state as DecimalType) >= 25) && (Windows.state != ON)) {
    postUpdate(SensiboMode, "cool")
    postUpdate(SensiboTarget, 23)
    sendCommand(SensiboState, ON)
  }

end

/*** Morning temperature ***/

rule "Morning temperature"
  
  when
    Time cron "0 0 7 * * ?"
  then

  sendCommand(SensiboTarget, 23)

end

/*** Weekday temperature ***/

rule "Weekday temperature"
  
  when
    Time cron "0 0 8 ? * MON-FRI"
  then

  sendCommand(SensiboTarget, 24)

end

/*** Workday evening temperature ***/

rule "Weekday evening temperature"
  
  when
    Time cron "0 0 16 ? * MON-FRI"
  then

  sendCommand(SensiboTarget, 23)

end

/*** Start blasting upstairs fan ***/
  
rule "Start blasting upstairs fan"
  
  when
    Time cron "0 0 17 * * ?"
  then
    
  if (Temperature.state >= 23) {
    sendCommand(UpstairsFan, "high")
  }

end

/*** Stop blasting upstairs fan ***/

rule "Stop blasting upstairs fan"

  when
    Time cron "0 * 19-20 * * ?"
  then
  
  if ((Elevation.state <= 4.5) && (UpstairsFan.state != "auto")) {
    sendCommand(UpstairsFan, "auto")
  }

end

/*** Set Sensibo for the night ***/

rule "Set Sensibo for night"

  when
    Time cron "0 0 22 * * ?"
  then

  if (Temperature.state >= 20) {
    postUpdate(SensiboMode, "cool")
    postUpdate(SensiboTarget, 23)
    sendCommand(SensiboState, ON)
  }
  else if (ForecastLow.state <= 10 && NestMode.state == "off") {
    postUpdate(SensiboMode, "heat")
    postUpdate(SensiboTarget, 22)
    sendCommand(SensiboState, ON)
  }
  else {
    sendCommand(SensiboState, OFF)
  }

end

I made another adjustment today to the error handling that kicks in when an update to a pod’s state fails. I had a write fail today, and when I looked at the logs I noticed that the error handling triggered but failed.

In a nutshell, it looks like you have to convert the item’s state into a String object in order to pass it to an item via sendCommand(), like this:

sendCommand(SensiboMode, SensiboMode.state.toString)

The new version is up on Github.

Micheal today I noticed Sensibo API calls are failing since 20:40 hours Sept 12th with error:

<html><head><title>400 InvalidState - Sensibo</title></head><body><h>400 InvalidState</h><p>parameter "acState" is invalid</p><body></html>

I did some manual troubleshooting and it seems it needs a space after the parameter colon and value in the sensibo-interface.rules file like:

    var String CommandURL = 'https://home.sensibo.com/api/v2/pods/' + PodID + '/acStates?apiKey=' + APIKey

    var String CommandState = '{"acState":{"on": ' + PodBoolean + ','
    var String CommandMode = '"mode": "' + SensiboMode.state + '",'
    var String CommandFan = '"fanLevel": "' + SensiboFan.state + '",'
    var String CommandSwing = ""

    if (SensiboSwing.state != "")
      { CommandSwing = '"swing": "' + SensiboSwing.state + '",' }

This should result in below API call difference.

acState":{“on”:false,“mode”:“cool” ===> acState":{“on”: false, “mode”: "cool"
Note the space before false and “cool”.

Can you check yours?