Customer Knowledge Base
Breadcrumbs

Controlling the Milesight WT101 LoRa TRV in Niagara

image-20251220-122853.png

A Forest Rock - Project Base Camp - Training Guide

Overview

The Milesight WT101 Thermostatic Radiator Valve (TRV) can send temperature readings into Niagara without any special work. However, if you want Niagara to write a new setpoint back to the TRV, that requires a LoRa downlink message.

Unlike simple LoRa devices that accept fixed commands (e.g., “turn relay on”), the TRV requires a dynamic payload—meaning the payload changes every time because the setpoint changes.

This guide explains the concept in a simple, practical way so you can create your own working solution in Niagara.


1. Why TRV Setpoints Are Different

While for some devices a set of fixed commands are used to drive a static action (close relay/open relay), the TRV self controls its operation and requires a temperature setpoint. Such a setpoint might need adjusting by a user or an automated time schedule, therefore a smarter conversion to build the payload on the fly might be needed, essentially to transform the desired temperature (setpoint) into a dynamic command needed by the TRV.

The WT101 is different because you need to send a temperature value, not a fixed text string. That value must be packaged into a special format before the LoRa gateway (UG65/UG67) will accept it.

To make this work, Niagara needs to build the payload on the fly based on your desired setpoint.


2. What the Gateway Expects

A Milesight UG65/UG67 sending a downlink requires JSON in this structure:

{"confirmed": false, "fport": 85, "data": "<base64 data>"}

The data field must be:

  1. A command (hex)

  2. Your setpoint (hex)

  3. Converted into a byte array

  4. Encoded into Base64

For example, to set 25°C:

  • TRV command: ff b1

  • Temperature in hex: 19 (because 25 decimal = 0x19)

  • Footer: 01 00

Full hex string becomes:

ff b1 19 01 00

When Base64‑encoded, this becomes:

/7EZAQA=

Which gives the final JSON:

{"confirmed": false, "fport": 85, "data": "/7EZAQA="}

Because your setpoint will change, Niagara must generate this Base64 string dynamically.

image-20260202-122807.png

See Property Sheet OutPayload String

image-20260202-122842.png

3. Making Niagara Generate the Payload Automatically

To avoid manual conversion, you can create a simple Niagara Program Object that:

  1. Takes your numeric setpoint (e.g., 18–30°C)

  2. Converts it into the correct hex format

  3. Converts the hex into bytes

  4. Converts the bytes into Base64

  5. Outputs the ready‑to‑send JSON payload

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

  • SetIn → your setpoint (integer)

  • Confirmed → true/false

  • FPort → typically 85

  • OutData → Base64 only

  • OutPayload → full JSON string to publish via MQTT

Below is the working code exactly as used in Niagara:

Java

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
  setOutData(base64String);
  
  // 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
  setOutPayload(jsonString);
}

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;
    }

Once compiled, the object automatically builds the correct downlink for you.

image-20260202-123201.png

4. What to Expect When Writing to the Valve

This is important:

Which means:

  • You send the new setpoint

  • The valve doesn’t apply it straight away

  • It first sends one more uplink with the old setpoint

  • THEN it accepts your new downlink

  • Future uplinks show the new setpoint

This delay is normal and is part of LoRa low‑power design.


5. Summary

Here’s the process in plain English:

  1. Niagara generates a dynamic Base64 payload from your setpoint.

  2. You send that payload via MQTT to the UG65/UG67.

  3. The valve updates the setpoint the next time it wakes up.

This guide provides a reliable template that you can drop straight into any Niagara project requiring dynamic LoRa TRV control.