Codec Decoder for zelsius C5 IUF

Hi everyone,

I’m struggling to create a working codec for the Zelsius C5-IUF device. I’ve received files from Zenner Support and attempted to write code using AI, but the output is incorrect. As I’m new to this, I would greatly appreciate any help in creating the correct code.

Please email me at eldionadvantx@gmail.com, and I’ll send the PDF files as I can’t upload them here.

The protocol is confidential? You can’t post it here? I didn’t find their protocol in their website.

No it’s not confidential but you have to reach to them to get it, and they only gave me the instruction documents but the files can’t be uploaded to this conversation.

Post the relevant details or upload it to a service (example). Split the url if the forum does not allow you to submit a URL.

here is the link for the files LoRa C5-IUF - Google Drive

Oh! That is absurd. Timestamp in the payload, CRC in the payload, there is 14 (?) variations of payload and I don’t understand the payloads. That’s what a I call a nightmare.

I would admire the person that would be able to write a decoder for that.

1 Like

And the worst part is that they only sent the two attached files, claiming they couldn’t provide further assistance. They said the files contained all the necessary information to build the decoder, but after two weeks of unsuccessful attempts, I’m still not been able to get value readings.

The only solution is to try to decode the examples locally with node.

This is the ‘master file’ with the payloads. Name it launch_decoder.js

// Trick from: https://stackoverflow.com/a/5809968
var fs = require('fs');
eval(fs.readFileSync("decoder.js")+'');

data = [ 0x02, 0x78, 0x87 ];
input = { fPort:"207", bytes:data };
var result = decodeUplink(input);
console.log(result);

data = [ 0x01, 0x05 ];
input = { fPort:"200", bytes:data };
var result = decodeUplink(input);
console.log(result);

data = [ 0x44, 0x5D, 0x64, 0x06, 0x12, 0x5D, 0x02, 0xAC, 0x02, 0x08, 0x59 ];
input = { fPort:"204", bytes:data };
var result = decodeUplink(input);
console.log(result);

data = [0x39, 0xc0, 0xdd, 0x10, 0x59, 0xf4, 0x00, 0x6b, 0x02, 0x65, 0x90]; 
input = { fPort:"204", bytes:data };
var result = decodeUplink(input);
console.log(result);

This is the decoder. Name it decoder.js

// https://forum.chirpstack.io/t/decoder-testing/10619/7

// Decode decodes an array of bytes into an object.
//  - fPort contains the LoRaWAN fPort number
//  - bytes is an array of bytes, e.g. [225, 230, 255, 0]
// The function must return an object, e.g. {"temperature": 22.5}

function decodeUplink(input) {
	// return raw un-encrypted payload
//  return { data: input };

/*
44 5D 64 06 12 5D 02 AC 02 08 59
445D6406125D02AC020859
"alt": 684,
"battery": "59",
"lat": "48.069016,
"lon": "8.538374",
"temp": 20.8
*/
  var decoded = {};

  if ( input.fPort == 204){
    lat = input.bytes[0] << 16;
    lat |= input.bytes[1] << 8;
    lat |= input.bytes[2];
    lat = lat/8388606 * 90;
    if (lat > 90) lat -= 180;
    decoded.latitude = lat.toFixed(6);
    
    lon = input.bytes[3] << 16;
    lon |= input.bytes[4] << 8;
    lon |= input.bytes[5];
    lon = lon/8388606 * 180;
    if (lat > 180) lon -= 360;
    decoded.longitude = lon.toFixed(6);
    
alt = input.bytes[6] << 8;
alt |= input.bytes[7];
    decoded.altitude = alt;
    
    temp = (input.bytes[8] & 0x0F) * 100;
    temp += ((input.bytes[9] & 0xF0) >> 4 ) * 10;
    temp += input.bytes[9] & 0x0F;
    
    if( input.bytes[8] & 80)
      temp /= -10;
    else
      temp /= 10;
    decoded.temp = temp;
    
    bat = ((input.bytes[10] & 0xF0) >> 4) * 10;
    bat += input.bytes[10] & 0x0F;
    decoded.battery = bat;
    decoded.accuracy = 10;
  } 
  else
    if (input.fPort == 207 || input.fPort == 205){
      temp = (input.bytes[0] & 0x0F) * 100;
      temp += ((input.bytes[1] & 0xF0) >> 4 ) * 10;
      temp += input.bytes[1] & 0x0F;
      
      if( input.bytes[0] & 80)
        temp /= -10;
      else
        temp /= 10;
      decoded.temp = temp;
      
      bat = ((input.bytes[2] & 0xF0) >> 4) * 10;
      bat += input.bytes[2] & 0x0F;
      decoded.battery = bat;
    }
  // if port 200 or 208 two bytes firmware version 0103 = 1.0.3 //
   else
    if (input.fPort == 200 || input.fPort == 208){
      if ( input.bytes[1] < 10 )
      decoded.firmware =  input.bytes[0] + ".0." + input.bytes[1]
      else
      decoded.firmware =  input.bytes[0] + "." + input.bytes[1] << 4; + "." + input.bytes[1];
    }

  return decoded;

}// function 

Run node launch_decoder.js to see the results.

Put in the data the data from examples and try to have the same results with the decoder.js.

If I understood correctly, they use the first byte to identify the type (and subtype) of the message instead of LoRaWAN port.

You have interest on all types of messages? What types of messages you receiving? (SP0 to SP13) or and AP1 and AP2?

Try to have success with one type to feel powerful and proceed.

it would be good if i could recive daily and monthly expenses and temperature values i belive are SP5, I’m currenlty have the device in diagnostic mode and sends SP01

How you know it’s SP01? From the first byte? Next 4 bytes seems to have the payload of SP01. Problem is that I can’t understand what kind of payload is and if it’s need decoding or it’s a value ready to use.

‘‘In diagnostic mode, in addition to the scenario 1 or scenario 2 packets special
diagnostic mode packets (SP 0.1) are sent.’’ It says this in documentation.

I repeat. How do you know it’s in SP01 mode? From the first byte?

So, this it the payload?

no. of Bytes content remark
0,5 packet type 0x00
0,5 packet subtype 0x01
4 Heating energy For a pure cooling meter always 0
4 Cooling energy For a pure heat meter always 0
4 Volume
1,5 AverageReturnTemperature int12 Temperature [°C] = value/10 Min: -204,8 Max: 204,7 Resolution: 0,1
1,5 MaxReturnTemperatur int12 Coding like AverageReturnTemperature
0,375 bit 0..2 DiagnosticIntervalSetup Used to choose the cycle time out of a table.
0,125 bit 3 empty tube
0,125 bit 4 device defect
0,125 bit 5 Device status and error bit (not defined)
0,125 bit 6 Device status and error bit (not defined)
0,125 bit 7 Device status and error bit (not defined)

yes from first byte, yeah this is the payload, i tried to decode it but still sends wrong values

Ok.

a. Can you post some real payloads to test?
b. Do you know the decoded values to verify the decoder?
c. Lets start with Heating Energy, I don’t want to re-read those documents. Can you understand the decoding method?
d. AverageReturnTemperature and MaxReturnTemperatur should be easy. It clearly states int12. Start with that.
e. Last byte it’s easy to decode.

Experiment here. Put the appropriate data and fPort.

I wrote that for a start.

data = [ 0x01,
0xf0,0x05,0x05,0x05, // heatingEnergy
0x00,0x05,0x05,0x05, // coollingEnergy
0x05,0x05,0x05,0x05, //volume
0xf3,0x17,0x03, // averageReturnTemperature + maxReturnTemperature
0x0f]; // debug
input = { fPort:1, bytes:data };
var result = decodeUplink(input);
console.log(result);


// Copy this to Chirpstack
function decodeUplink(input) {
  var decoded = {};
  
  if (input.fPort == 1 ){
    decoded.type    = input.bytes[0] & 0x0f;
    decoded.subtype = input.bytes[0] & 0xf0;
    decoded.heatingEnergy = input.bytes[1] << 24 | input.bytes[2] << 16 | input.bytes[3] << 8 | input.bytes[4];
    decoded.coollingEnergy = input.bytes[5] << 24 | input.bytes[6] << 16 | input.bytes[7] << 8 | input.bytes[8];
    decoded.volume = input.bytes[9] << 24 | input.bytes[10] << 16 | input.bytes[11] << 8 | input.bytes[12];
    decoded.averageReturnTemperature = input.bytes[13] << 8 | input.bytes[14] & 0x0f;
    if ( ( input.bytes[13] & 0x80 ) == 0x80) {
      decoded.averageReturnTemperature *= -1;
    }
    // I don't know where is the half byte. I suppose upper bits if bytes[14]
    decoded.maxReturnTemperature = (input.bytes[14] & 0xF0 ) << 8 | input.bytes[15];
    if ( ( input.bytes[14] & 0x8 ) == 0x8) {
      decoded.maxReturnTemperature *= -1;
    }
    
    decoded.bytes   = input.bytes.length;
  }

  return decoded;

}// function 

yes here i have the latest payload i recived:
“AQEAAAAAAAAAicwCALaBGwA=”
“AQEAAAAAAAAAEswCALexGwA=”
“AQEAAAAAAAAAnMsCALzhGwA=”
“AQEAAAAAAAAAJssCAL0RHAA=”
“AQEAAAAAAAAAr8oCAMJBHAA=”

I tested the code with these but the values are not correct.

function decodeUplink(input) {
  var decoded = {};
  var warnings = [];
  var errors = [];

  try {
    if (input.fPort !== 1) {
      errors.push("Invalid fPort. Expected fPort 1.");
      return { data: {}, warnings: warnings, errors: errors };
    }

    if (!input.bytes || input.bytes.length !== 17) {
      errors.push("Invalid payload length. Expected 17 bytes.");
      return { data: {}, warnings: warnings, errors: errors };
    }

    // Packet type and subtype
    decoded.type = input.bytes[0] & 0x0f;
    decoded.subtype = input.bytes[0] & 0xf0;

    // Heating Energy (little-endian, scaled)
    decoded.heatingEnergy = input.bytes[4] << 24 | input.bytes[3] << 16 | input.bytes[2] << 8 | input.bytes[1];
    decoded.heatingEnergy *= 1; // Example scaling factor

    // Cooling Energy (little-endian, scaled)
    decoded.coollingEnergy = input.bytes[8] << 24 | input.bytes[7] << 16 | input.bytes[6] << 8 | input.bytes[5];
    decoded.coollingEnergy *= 1; // Example scaling factor

    // Volume (little-endian, scaled)
    decoded.volume = input.bytes[12] << 24 | input.bytes[11] << 16 | input.bytes[10] << 8 | input.bytes[9];
    decoded.volume *= 0.001; // Example scaling factor

    // Temp Inlet (12-bit signed, scaled)
    decoded.tempInlet = input.bytes[13] << 4 | (input.bytes[14] >> 4);
    if ((decoded.tempInlet & 0x800) == 0x800) {
      decoded.tempInlet -= 0x1000; // Sign extension
    }
    decoded.tempInlet *= 0.1; // Scaling factor

    // Temp Outlet (12-bit signed, scaled)
    decoded.tempOutlet = (input.bytes[14] & 0x0F) << 8 | input.bytes[15];
    if ((decoded.tempOutlet & 0x800) == 0x800) {
      decoded.tempOutlet -= 0x1000; // Sign extension
    }
    decoded.tempOutlet *= 0.1; // Scaling factor

    // Flow (12-bit unsigned, scaled)
    decoded.flow = (input.bytes[15] & 0xF0) >> 4 | input.bytes[16] << 4;
    decoded.flow *= 0.001; // Scaling factor

    // Debug Information
    decoded.debug = input.bytes[16];

    // Total Bytes
    decoded.bytes = input.bytes.length;
  } catch (e) {
    errors.push("Error decoding payload: " + e.message);
    return { data: {}, warnings: warnings, errors: errors };
  }

  return {
    data: decoded,
    warnings: warnings,
    errors: errors
  };
}                           

The code provides correct volume, heating energy, and cooling energy, but the flow and temperatures are inaccurate. Despite prolonged hot water flow, the heating and cooling energy values remain static at 1 kWh and 0 kWh respectively, even though the flow reading changes (currently 0.459), with an inlet temperature of 47.1 and an outlet temperature of 41.44.

Ok.

I suppose those are the bytes

for i in `cat zenner`; do echo $i; echo $i | base64 -d | xxd; done
AQEAAAAAAAAAicwCALaBGwA=
00000000: 0101 0000 0000 0000 0089 cc02 00b6 811b  ................
00000010: 00                                       .
AQEAAAAAAAAAEswCALexGwA=
00000000: 0101 0000 0000 0000 0012 cc02 00b7 b11b  ................
00000010: 00                                       .
AQEAAAAAAAAAnMsCALzhGwA=
00000000: 0101 0000 0000 0000 009c cb02 00bc e11b  ................
00000010: 00                                       .
AQEAAAAAAAAAJssCAL0RHAA=
00000000: 0101 0000 0000 0000 0026 cb02 00bd 111c  .........&......
00000010: 00                                       .
AQEAAAAAAAAAr8oCAMJBHAA=
00000000: 0101 0000 0000 0000 00af ca02 00c2 411c  ..............A.
00000010: 00                                       .

Let’s break down first example and first bytes.

AQEAAAAAAAAAicwCALaBGwA=
00000000: 0101 0000 0000 0000 0089 cc02 00b6 811b  ................
00000010: 00

Heating Energy and Cooling Energy (first 8 bytes) are every time 00. It seems that after 10th byte (volume) we have updated values :frowning:

For other values experiment with the example I gave. Add the data there and experiment.