How to control the TRV WT101 in Niagara

Compared to other devices we worked with before, this one will need the downlink to be dynamically changing with the desired setpoint and a conversion made to get the right data. Let's have a look


The IoT industry is more and more blending in with the Building Management and integrating to offer some of the benefits of this new technology while allowing for the full integration and customization achievable using frameworks like Niagara or edge monitoring systems such as Link Edge.

While pure monitoring is achievable with the Milesight UG65 and UG67 gateways using HTTP Rest API (see THIS ARTICLE for an example with Niagara), if you need to write back to a LoRa device you will need to use MQTT with such gateways.

We have a few articles on MQTT like THIS ONE. If you have got no idea what MQTT is, we would strongly recommend reading that before you start.

Let say you are familiar with MQTT, we then get to some other articles which extend the "monitoring only" concepts with the Milesight LoRa sensors and get to the "writing" side of things. This is a bit more complicated, as writing back to a sensor ("Downlink" in LoRa terms) requires to prepare a payload designed specifically for the Gateway (UG65/UG67) to address your command properly to the desired sensor.

We already have 2 articles on writing back to LoRa devices. They are similar and good starts to understand what we are going to talk about in this article, check them out HERE (first one) and HERE (second one).

So why another article you might ask?

The first 2 are sending a fixed payload. You want to switch ON? send "this string" and the contact will close sort of thing.

So check them out to see how that works in details, and only then please keep reading.

This article in fact will add a few notions, as with a TRV (Thermostatic Radiator Valve) you might want to send a dedicated Setpoint, and to achieve that we will need to make that Downlink Payload dynamic to send our desired Setpoint value.


What needs to be dynamic for a Setpoint to work

Did you read the part above, and all the articles linked? If so, then let see what is different on this one.

You should have noticed that all these sensors require you to package your data to be sent into a JSON payload, where the data is packaged into a Base64 set.

So, with that notion in mind, let's have a look at the documentation of the WT101 TRV device. We can find that within the downlink commands available, there is the "b1" "Set Target Temperature" command available, with the example here below

So, in essence, we need to send "ff b1 19 01 00" to get our setpoint set to 25 degrees Celsius (value 19 in hexadecimal notation)

As you have already seen in the other 2 writing articles, we will need to convert this in Base64 to send it out to our Gateway, specifically in this format:

{"confirmed":[true/false],"fport":[Data Port],"data":"[Data in Base64]"}

Converting our "ff b1 19 01 00" to Base 64 we would get this "/7EZAQA="

Which if put into an example JSON payload will then make our Downlink payload request looking like this:


This is all in the other 2 articles linked in the last part of the introduction, so please have a look at those if the above is not clear.


So, we cannot really fix this whole payload into a "String" and send it, as we might want to change that setpoint. So we are going to try creating that "Data in Base64" value dynamic based on a numeric setpoint input (8 bit integer value "0 to 255", to fit within the 1 byte "00 to FF" as desired)


The Niagara part, introduction

We are going to have our setpoint, an integer number that should be a value between 0 and 255, more likely 10 to 30 degrees Celsius for this specific instance.

We will need to create a custom program block to get the numeric setpoint input and provide us with the desired string output that we can use for our payload to be sent via MQTT to the UG65/UG67.

This is not an easy topic, and as Innon we are planning on creating ready made blocks that you will be able to use from a special Innon library.

For now, if you are eager to start or you simply might want to push yourself to learn something new, this article is going to be helping for sure.

We talked about program objects with and introduction article that you can find RIGHT HERE and that I would strongly recommend reading to get your bearings around what this is.


Let's code

You have got all the notions from the articles linked above: it is clear what we need to do and why.

Also, we are going to use the "java.util.Base64" utility class. In this article I used Niagara version

Our end goal is to create a block that will provide us the Payload in JSON that we need to send to the UG65/UG67, provided the Setpoint and with the possibility of setting up the "confirmed" and the "fPort" data


We start by creating our program object (see previously linked articles). I called mine "SetpointPayload".

I create 5 new slots:

  • SetIn (setpoint input) flagged as "summary" and "Execute On Change", and baja type "Integer"
  • Confirmed (the "confirmed" true/false setting) flagged as "summary" and "Execute On Change", and baja type "Boolean"
  • FPort (the fPort setting for the sensor) flagged as "summary" and "Execute On Change", and baja type "Integer"
  • OutData (an output providing just the data in Base64) flagged as "summary" and "Read Only, and baja type "String"
  • OutPayload (an output providing the full JSON payload) flagged as "summary" and "Read Only, and baja type "String"


With that out of the way, use the code below on the "Edit" tab

public void onStart() throws Exception
  // start up code here

public void onExecute() throws Exception
  // execute code (set executeOnChange flag on inputs)
  // Your integer value (1 byte)
  int byteValue = getSetIn();
  // Convert the byte to a hex string
  String hexString = Integer.toHexString(byteValue);
  // Pad the hex string to ensure it has two characters
  if (hexString.length() == 1) {
      hexString = "0" + hexString;
  // Concatenate "ffb1" to the beginning and "0100" to the end of the hex string
  String finalHexString = "ffb1" + hexString + "0100";
  // Convert the hex string to bytes
  byte[] byteArray = hexStringToByteArray(finalHexString);
  // Encode the byte array to a base64 string
  String base64String = java.util.Base64.getEncoder().encodeToString(byteArray);
  // Push the value to the output
  // Additional inputs
  boolean confirmed = getConfirmed();
  int fPort = getFPort();
  // Construct the JSON string
  String jsonString = "{\"confirmed\": " + confirmed + ", \"fport\": " + fPort + ", \"data\": \"" + base64String + "\"}";
  // Push the value to the output

public void onStop() throws Exception
  // shutdown code here

public static byte[] hexStringToByteArray(String hexString) {
        int len = hexString.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
        return data;


Compile the block and enjoy


Notes on the valve downlink messages

When writing a new setpoint for example, being the valve battery powered and in Class A, it takes a bit of time for a downlink message (i.e. the setpoint) to be taken by the valve, as it will be waiting for the next time it communicates with the gateway to process it. For this reason, you will receive most probably another uplink payload from the valve with the previous setpoint, then the valve will process your new downlink and save the new setpoint. From that moment onward every new uplink payload you will receive will have the new setpoint, unless obviously someone changes it rotating the valve setpoint regulation physically.

LoRa is not a fast protocol by design, this behavior is normal and is made this way to guarantee the best battery life you can get.