Request: ADAX thermostat/radiator binding

Hi
I have a number of Adax Wifi radiators in my house and would love to have these integrated into my OpenHAB installation. But, I’m not a developer and have gotten most of my other OpenHAB stuff working based on browsing the forum and kind of copy/paste a lot of solutions. So, my programming knowledge and tech understanding is limited. As far as I know, there’s currently no way to integrate these radiators with existing bindings in OpenHAB.

Since I purchased these radiators, Adax has publicly released an API to be able to integrate with the thermostats - Getting Started with the Adax API

So, my questions are

  1. Is an integration of these radiators already possible with something existing (that I have missed)?
  2. If not, can a binding be created based on this API?
  3. Are you willing to help me do it?

I’m willing to help as much as I can, but I’m afraid I’m somewhat lost when it comes down to this tech level. I know there are guides I could read how to do this - but that’s in theory. In reality I would have a hard time understanding the process without proper (and patient) guidance.

Any help would be appreciated. Thanks.

1 Like

Submit a request on bountysource to encourage a volunteer developer to develop a binding. That is the current path for non-developers who wish new addons.

Without a publicly released API though chances are slim. Web screen scraping is discouraged for official addons even if it woks short term. There are too many changes that could break things.

Thank you Bruce
I will check out bountysource.

There is a released and publicly available API (as far as I can tell) - the one I linked to in my first post.

Sorry, I somehow misread that. - Not enough :coffee: :frowning:

  1. You could probably use the HTTP binding for this. If not, you can use the Exec binding or executeCommandLine Action to call the example Python scripts provided at that link.

  2. Yes, but it will require finding a volunteer who both has Adax devices and has the desire and ability to create a binding.

  3. I have neither Adax devices nor the desire a and ability to create a binding. But I can help debug rules and to some degree HTTP binding and Exec binding issues.

1 Like

I have now managed to make it work on my OpenHAB 2.5 (openhabian), using python scripts and executeCommandLine. Here is my setup, currently for one room (since I only have one oven) … sorry for the weird mix of English and Norwegian:

items:

Number  AdaxHillevis_Id         "Adax room id [%d]"
Switch  AdaxHillevis_Heating    "Status av ovnen [%s]"  <heating>
Number  AdaxHillevis_RoomTemp   "Romtemperatur  [%.1f °C]"  <temperature>
Number  AdaxHillevis_Setpoint   "Ønsket temp. [%.0f °C]"  <temperature>
Number  AdaxHillevis_AdaxSetPt  "Setpoint Adax cloud [%.0f °C]"  <temperature>

AdaxHillevis_Id includes the Adax API id of the room (temperature control is per room not per device). I set it in a “System started” rule.

adax_get_room_status.py:

#!/usr/bin/python3
import requests
import sanction
import sys
import json

if len(sys.argv) < 2:
    print("Error: needs the room ID as an argument")
    quit()
ROOM_ID = int(sys.argv[1])

# for values, see Adax WiFi app, Account Section
CLIENT_ID = "xxxxxx"
CLIENT_SECRET = "123456789abcd"
API_URL = "https://api-1.adax.no/client-api"

def get_token():
    # Authenticate and obtain JWT token
    oauthClient = sanction.Client(token_endpoint = API_URL + '/auth/token')
    oauthClient.request_token(grant_type = 'password', username = CLIENT_ID, password = CLIENT_SECRET)
    return oauthClient.access_token

def get_room_status(roomId, token):
    headers = { "Authorization": "Bearer " + token }
    response = requests.get(API_URL + "/rest/v1/content/", headers = headers)
    status = response.json()
    for room in status['rooms']:
        if room['id'] == roomId:
            return room
    return {}  # room not found

token = get_token()
status = get_room_status(ROOM_ID, token)
print(status)

adax_set_room_tg_temp.py

#!/usr/bin/python3
import requests
import sanction
import sys
if len(sys.argv) < 3:
    print("Error: needs 2 arguments: room ID and the target temperature")
    quit()
ROOM_ID = int(sys.argv[1])
TG_TEMP = round(float(sys.argv[2]), 2)

# for values, see Adax WiFi app, Account Section
CLIENT_ID = "xxxxxx"
CLIENT_SECRET = "123456789abcd"
API_URL = "https://api-1.adax.no/client-api"

def get_token():
    # Authenticate and obtain JWT token
    oauthClient = sanction.Client(token_endpoint = API_URL + '/auth/token')
    oauthClient.request_token(grant_type = 'password', username = CLIENT_ID, password = CLIENT_SECRET)
    return oauthClient.access_token

def set_room_target_temperature(roomId, temperature, token):
    # Sets target temperature of the room
    headers = { "Authorization": "Bearer " + token }
    json = { 'rooms': [{ 'id': roomId, 'targetTemperature': round(100 * temperature) }] }
    requests.post(API_URL + '/rest/v1/control/', json = json, headers = headers)

token = get_token()
set_room_target_temperature(ROOM_ID, TG_TEMP, token)

Note that one needs to install the sanction package for user openhab, which I solved by installing it system-wide using sudo pip3 install sanction.

rules:

rule "Update Adax status for Hillevi's room from Adax cloud"
when
        Time cron "0 0/10 * * * ?"  // every 10 minutes
then
        val String roomStatus = executeCommandLine("/etc/openhab2/misc/adax_get_room_status.py " + AdaxHillevis_Id.state.toString, 5000)
        logDebug("Adax", "roomStatus = " + roomStatus)

        val roomTemp = Double::parseDouble(transform("JSONPATH", "$.temperature", roomStatus)) / 100
        val Number tgTemp = Double::parseDouble(transform("JSONPATH", "$.targetTemperature", roomStatus)) / 100
        val Boolean enabled = Boolean::parseBoolean(transform("JSONPATH", "$.heatingEnabled", roomStatus))
        val roomName = transform("JSONPATH", "$.name", roomStatus)
        logInfo("Adax", "update for " + roomName + ": temp = " + roomTemp.toString + "°C, set point = " + tgTemp.toString + "°C, heater enabled = " + enabled.toString)

        AdaxHillevis_AdaxSetPt.postUpdate(tgTemp)
        AdaxHillevis_RoomTemp.postUpdate(roomTemp)
        if (enabled) {
                AdaxHillevis_Heating.postUpdate(ON)
        } else {
                AdaxHillevis_Heating.postUpdate(OFF)
        }
        // wait a bit, so _AdaxSetPt is set, to avoid calling adax_set_room_tg_temp.py
        Thread::sleep(200)
        AdaxHillevis_Setpoint.postUpdate(tgTemp)
end

rule "Send new setpoint for Hillevis room to Adax cloud"
when
        Item AdaxHillevis_Setpoint changed
then
        if (newState != AdaxHillevis_AdaxSetPt.state) {
                // send the new setpoint to the Adax cloud and update AdaxHillevis_AdaxSetPt
                logInfo("Adax", "sending updated setpoint " + newState.toString + " to Adax cloud")
                executeCommandLine("/etc/openhab2/misc/adax_set_room_tg_temp.py " + AdaxHillevis_Id.state.toString + " " + newState.toString)
                AdaxHillevis_AdaxSetPt.postUpdate(newState)
        }
end

Note that executeCommandLine() changed syntax in OH3.

This setup needs the _AdaxSetPt to be set before updating _Setpoint, otherwise the second rule triggers a needless update to the Adax cloud. It is not very elegant, but the best I managed…
This should be all…

One question for Rich: this works for one room, but with several rooms, I would have to duplicate the rules - but I am pretty sure there is a way to write rules that can work for more things, provided they have the same naming scheme for items - can you help here?

Moreover, if I had more rooms, I would probably rewrite the ‘get’-script so that i returns the json for all rooms and then try to write a rule that updates all items by parsing the json - but again, this requires better knowledge of the rules language than I have…

1 Like

A small update: the above solution had a problem that when I changed the setpoint using for ex. the Setpoint widget in the Classic UI, it will try to send the update after each change, which was causing problems.
This is solved using a timer, so the update is sent 10 seconds after the last change:

var Timer adaxHillevisSetPtTimer = null

rule "Send new setpoint for Hillevis room to Adax cloud"
when
        Item AdaxHillevis_Setpoint changed
then
        // cancel any planned updates
        if (adaxHillevisSetPtTimer !== null) {
                adaxHillevisSetPtTimer.cancel()
        }
        if (newState != AdaxHillevis_AdaxSetPt.state) {
                logInfo("Adax", "received a new setpoint " + newState.toString + " for Hillevis room")
                // wait for 10 seconds, in case we get a new value
                adaxHillevisSetPtTimer = createTimer(now.plusSeconds(10)) [|
                        // send the new setpoint to the Adax cloud and update AdaxHillevis_AdaxSetPt
                        logInfo("Adax", "sending updated setpoint " + newState.toString + " to Adax cloud")
                        executeCommandLine("/etc/openhab2/misc/adax_set_room_tg_temp.py " + AdaxHillevis_Id.state.toString + " " + newState.toString)
                        AdaxHillevis_AdaxSetPt.postUpdate(newState)
                ]
        }
end
1 Like

I just stumbled over your ADAX implementation as I bought new ADAX-panels and are keen to try your scripts.
In which folder should the python script be copied on a openhabian system (running on RaspBerry Pi)?

As you can see in the examples, the python scripts are in /etc/openhab2/misc/. This was on OH2, running on opanhabian.
I expect this to work on OH3 as well, but only if the DSL scripts are saved to files (like in OH2) - it won’t work in an UI-created rule, since it uses a global timer.

I am currently in a process of slowly re-creating the whole setup in OH3. For the ADAX integration, I want to test HabApp, so I can do it completely in Python…

1 Like

Thanks for your fast answer. I haven’t seen/recognized the file-location in the examples.
As I am also still on OH2, I will start trying in coming weekend. 3 additional ADAX-heaters just arrived yesterday.

Update two year later, on OH4

I have now managed to do the communication completely in a UI-defined JS script:

var API_URL = "https://api-1.adax.no/client-api"
var tokenEndpoint = "/auth/token"
var statusEndpoint = "/rest/v1/content/"
var controlEndpoint = "/rest/v1/control/"

var get_token = function() {
  var contentType = "application/x-www-form-urlencoded"
  var content = ""

  if (items.getItem('adax_refresh_token').isUninitialized) {
    // get the initial token, using user name and password
    var CLIENT_ID = "123456"          // UPDATE
    var CLIENT_SECRET = "qwertyuiop"  // UPDATE
    content = "grant_type=password" + "&username="+CLIENT_ID + "&password="+CLIENT_SECRET
  } else {
    // update token, using the refresh token
    var rToken = items.getItem('adax_refresh_token').state
    content = "grant_type=refresh_token" + "&refresh_token="+rToken
  }

  var responseStr = actions.HTTP.sendHttpPostRequest(API_URL + tokenEndpoint, contentType, content)
  var json = JSON.parse(responseStr)
  items.getItem('adax_access_token').postUpdate(json.access_token)
  items.getItem('adax_refresh_token').postUpdate(json.refresh_token)

  return json.access_token
}

token = get_token()

// get current status
var headers = new Map()
headers.set("Authorization", "Bearer " + token)
console.info("ADAX test: headers = ", headers, ", type = ", typeof headers)

responseStr = actions.HTTP.sendHttpGetRequest(API_URL + statusEndpoint, headers, 10000)
var json = JSON.parse(responseStr)

// get temperature of the first room
var hillevis = json.rooms[0]
var roomId = hillevis.id
var setPoint = hillevis.targetTemperature / 100
var ovenTemp = hillevis.temperature / 100
console.info("ADAX test: target temp. is ", setPoint, ", actual temp. is ", ovenTemp)

// set a new setpoint
var newSetPoint = setPoint - 1
var contentType = "application/json"
json = {
  'rooms': [
    {
      'id': roomId,
      'targetTemperature': 100 * newSetPoint
    }
  ]
}
content = JSON.stringify(json)

responseStr = actions.HTTP.sendHttpPostRequest(API_URL + controlEndpoint, contentType, content, headers, 10000)
var json = JSON.parse(responseStr)

Since the tokens last 24 hours, both ‘adax_access_token’ and ‘adax_refresh_token’ are set to expire after 23 hours.

For my use, I will cut this to pieces and call the update every couple of minutes and the setpoint-setting each time a related OH item get updated…

1 Like

Thanks for sharing, this was very useful.

You mentioned a couple of minute update, but did you find that the API data is only updated every 6 minutes?

No, I did not know that - thanks for info.

So you are basically replacing “Adax room id [%d]” with the room ID? in the items file?

And by the way, thanks a lot for your help. The python scripts works like a charm. Now I only have to figure out how to get it to work with OpenHAB. :slight_smile:

I am completely new to OH4. Where did you place the JavaScript? Is it a rule or how can I utilise it in OH4? Any help in the right direction would be highly appreciated. :slight_smile:

You need to split it into multiple rules. The first half (before // get current status), you should run once per day and on system startup to update your access token.

Then use the next bits as examples for rules to pull the data every 6 minutes and to apply a temperature change. e.g to get the temperature reading.

var API_URL = "https://api-1.adax.no/client-api"
var statusEndpoint = "/rest/v1/content/?withEnergy=1"
var controlEndpoint = "/rest/v1/control/"

token = items.getItem('adax_access_token').state
  
// get current status
var headers = new Map()
headers.set("Authorization", "Bearer " + token)

responseStr = actions.HTTP.sendHttpGetRequest(API_URL + statusEndpoint, headers, 10000)
var json = JSON.parse(responseStr)

// get temperature of the first room
var hillevis = json.rooms[0]
var roomId = hillevis.id
var setPoint = hillevis.targetTemperature / 100
var ovenTemp = hillevis.temperature / 100

// Store values in items
items.getItem('heater2_Temperature').postUpdate(ovenTemp)
items.getItem('heater2_SetTemperature').postUpdate(setPoint)

And to set:

var API_URL = "https://api-1.adax.no/client-api"
var statusEndpoint = "/rest/v1/content/"
var controlEndpoint = "/rest/v1/control/"

token = items.getItem('adax_access_token').state

// get roomId
var headers = new Map()
headers.set("Authorization", "Bearer " + token)

responseStr = actions.HTTP.sendHttpGetRequest(API_URL + statusEndpoint, headers, 10000)
var json = JSON.parse(responseStr)

var hillevis = json.rooms[0]
var roomId = hillevis.id

// set a new setpoint
var newSetPoint = items.getItem('heater2_SetTemperature').rawState 
var contentType = "application/json"
json = {
  'rooms': [
    {
      'id': roomId,
      'targetTemperature': 100 * newSetPoint
    }
  ]
}
content = JSON.stringify(json)

responseStr = actions.HTTP.sendHttpPostRequest(API_URL + controlEndpoint, contentType, content, headers, 10000)
var json = JSON.parse(responseStr)
console.info("ADAX test: Set temperature: ", json.rooms[0].status)
1 Like

Success. Thanks a lot for your help.

So you have a cronjob that refreshes the the access token once every day and another cronjob to update the state every 10 minutes or so?

2024-01-22 13:04:19.424 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item ‘heater2_Temperature’ changed from NULL to 17.02

2024-01-22 13:04:19.425 [INFO ] [openhab.event.ItemStateChangedEvent ] - Item ‘heater2_SetTemperature’ changed from NULL to 18

I have now finally found time to set it up as well and ended up with solution that looks very much like that of @mjptec.
In my case, the “get” rule is called every 5 minutes and the “set” rule every time the setpoint item changes. This, however, poses the following problems:

  1. if the “get” script changes the setpoint, it triggers the “set” rule, so the value would be unnecessarily sent back to the server. To avoid it, I disable the rule prior to updating the setpoint item and re-enable it afterwards. The code for this is:
    rules.setEnabled('adax_post_setpoint_hillevis', false), where 'adax_post_setpoint_hillevis' is id of my “set” rule.
  2. the other way is trickier, because of the API delay: when the setpoint is changed (for ex. using GUI), it triggers the “set” rule which sends the new value to ADAX. However, if it really takes up to 6 minutes to update, there is a high probability that the “get” rule will trigger before the update and therefore changes the setpoint item back to the old value. To avoid this, I do the following in the “set” rule:
    • disable the “get” rule
    • start a timer that enables the rule after 10 minutes (just to be sure)

The code for the last part, generated by Blockly, is:

rules.setEnabled('adax_update_setpoint_hillevis', false);
if (cache.private.exists('AdaxApiUpdateTimer') === false || cache.private.get('AdaxApiUpdateTimer').hasTerminated()) {
  cache.private.put('AdaxApiUpdateTimer', actions.ScriptExecution.createTimer('AdaxApiUpdateTimer', time.ZonedDateTime.now().plusMinutes(10), function () {
    rules.setEnabled('adax_update_setpoint_hillevis', true);
    cache.private.remove('AdaxApiUpdateTimer');
  }));
} else {
  cache.private.get('AdaxApiUpdateTimer').reschedule(time.ZonedDateTime.now().plusMinutes(10));
};

Hope this helps…

1 Like

For that reason I use .postUpdate in the get rule and then for the trigger on the set rule use ‘when receives a command’.
The former will not trigger the latter.

The delay is quite frustrating though. It makes my charts a bit of a mess! Having some kind of waiting-for-update flag to stop the get rule overwriting it would be a good idea.