BARANI MeteoX IoT Pro Gen2 -> allMETEO via akenza (Master Guide)

BARANI MeteoX Gen2 -> allMETEO via akenza - Master Integration Guide

BARANI DESIGN - Integration Guide

MeteoX IoT Pro Gen2 → allMETEO via akenza

One pipeline for every Gen2 device. akenza decodes for its own dashboard and forwards the raw payload to allMETEO, which decodes it server-side by DevEUI. Setup and webhook are shared; each device adds only its decoder.

Applies to: MeteoHelix, MeteoWind (HNWL), MeteoRain, MeteoAG, MeteoALTIM — IoT Pro Gen2. Decoders from github.com/barani-design/P_SRC_device_decoders.

1How this works

Every BARANI MeteoX IoT Pro Gen2 device sends a LoRaWAN uplink whose payload is a compact binary frame. The goal is to land that data in the legacy allMETEO platform (weather.allmeteo.com) the same way the existing fleet does. The path is: sensor -> LoRaWAN network -> akenza (decode + webhook) -> gw.allmeteo.com/tti/uplink -> allMETEO.

The key idea - allMETEO decodes the raw payload itself

allMETEO does not need akenza's decoded fields. Its tti/uplink.php endpoint takes the raw frm_payload (base64) and decodes it server-side, keyed on the device's dev_eui. So the akenza webhook is identical for every Gen2 device - it just forwards dev_eui + frm_payload + f_port + f_cnt. Only the akenza decoder (for akenza's own dashboard) differs per device, and that is the only per-device part of this guide.

Because of that, this one guide covers all Gen2 devices: the akenza setup (Parts 2-3), the webhook (Part 5) and verification (Part 6) are common; each device just gets its own decoder from the appendix.

2Prerequisites

  • An akenza organisation and workspace, with the device's LoRaWAN connector configured (e.g. Swisscom CaaS / Actility roaming, or your own network server).
  • The device registered on the LoRaWAN network with its DevEUI, JoinEUI/AppEUI and AppKey (OTAA).
  • The station provisioned in allMETEO under its dev_eui, with the correct device type - this is what tells allMETEO how to decode the frm_payload.
  • The device's adapted decoder from the appendix for its type.
akenza org and workspace - all devices live under one workspace (example: MeteoWind).
akenza org and workspace - all devices live under one workspace (example: MeteoWind).
allMETEO must know the device type

allMETEO decodes the frm_payload according to the device type registered for that dev_eui. If a given Gen2 type is not yet supported on your allMETEO instance, akenza will still forward correctly but the Data tab stays empty until allMETEO can decode it. Confirm each station type is provisionable before rollout.

3Add the device in akenza

Create the device under your workspace and attach it to the LoRaWAN connector, then confirm it is joining and delivering uplinks.

  • Devices -> Add device - set the DevEUI; choose the device type (one per MeteoX model, holding that model's decoder).
  • Attach the LoRaWAN connector (Swisscom CaaS shown) so uplinks route into akenza.
  • Confirm OTAA join and that periodic uplinks arrive on port 1 (Message Logs).
Device connectivity details - the LoRaWAN connector (Swisscom CaaS shown).
Device connectivity details - the LoRaWAN connector (Swisscom CaaS shown).
Swisscom roaming routing applied to the device.
Swisscom roaming routing applied to the device.

4Uplink decoder (per device)

akenza runs a JavaScript uplink decoder on the device type. Each adapted decoder in the appendix is the BARANI repo decodeUplink() kept verbatim, plus a small generic consume() wrapper that makes it run in akenza and adds the forwarding fields. Keeping the decode logic untouched means it stays in sync with barani-design/P_SRC_device_decoders.

What the wrapper does

The wrapper is the same for every device:

function consume(event) {
  var port = event.data.port;
  var bytes = Hex.hexToBytes(event.data.payloadHex);
  var clean = {};
  if (port === 1) {
    var out = decodeUplink({ bytes: bytes, fPort: port });
    var d = (out && out.data) || {};
    for (var k in d) {
      if (!Object.prototype.hasOwnProperty.call(d, k)) continue;
      var nk = String(k).replace(/[^A-Za-z0-9_]/g, "_");   // spaces/punct -> _
      if (/^[0-9]/.test(nk)) nk = "m_" + nk;               // digit-leading -> valid
      clean[nk] = d[k];
    }
    if (typeof d.index !== "undefined") clean.f_cnt = d.index;
  }
  clean.frm_payload = bytesToBase64(bytes);   // forwarded to allMETEO (any port)
  clean.payload_hex = bytesToHex(bytes);
  clean.f_port = port;
  emit("sample", { data: clean, topic: "default" });
}
  • On port 1 (periodic data) it runs the device's decodeUplink() and copies its fields out for akenza's dashboard.
  • It sanitizes keys so akenza's database accepts them: any key starting with a digit gets an m_ prefix, and spaces/punctuation become _. (akenza rejects keys that start with a digit - e.g. 00A_device becomes m_00A_device.)
  • It appends frm_payload (base64), payload_hex, f_port and f_cnt (from the frame index). These four are what the webhook forwards.
  • On other ports (e.g. port 2 service messages) it forwards the raw payload without decoding.

Install it

  • Device Type -> Uplink -> open the script editor.
  • Select all (Ctrl/Cmd-A) and delete, then paste the appendix decoder. A partial paste leaves two decodeUplink() / consume() definitions and the old one wins - so clear the editor fully first.
  • Run script with a real port-1 frame and confirm the device fields plus frm_payload / f_port / f_cnt appear, then Save.
Device Type -> Uplink: paste the adapted decoder here, then Save.
Device Type -> Uplink: paste the adapted decoder here, then Save.
Emitted Samples after a clean decode - device fields plus frm_payload / f_port / f_cnt.
Emitted Samples after a clean decode - device fields plus frm_payload / f_port / f_cnt.
If akenza Data Overview shows only frm_payload

That means the digit-leading-key rejection hit (you will see data key '..' is invalid warnings in Logs) or the old script is still saved. The appendix decoders already sanitize keys; re-paste with a full select-all and Save.

5Webhook to allMETEO (identical for every device)

Add a Webhook output connector to the device's Data Flow and give it the unwrapped TTN-v3 body. This is the single most important detail and it is the same for all Gen2 devices.

  • Data Flow -> add Webhook output -> method POST, URL https://gw.allmeteo.com/tti/uplink, no auth.
  • Set Custom payload to the template in Appendix W.
  • Save connector and Save Data Flow.
The webhook output connector in the Data Flow.
The webhook output connector in the Data Flow.
Webhook connector: method POST, URL gw.allmeteo.com/tti/uplink.
Webhook connector: method POST, URL gw.allmeteo.com/tti/uplink.
The Data Flow ties device type + connector + outputs together.
The Data Flow ties device type + connector + outputs together.
Use custom payload -> the unwrapped TTN-v3 body (Appendix W).
Use custom payload -> the unwrapped TTN-v3 body (Appendix W).
The body must be UNWRAPPED - this is the crux

allMETEO's tti/uplink.php reads end_device_ids and uplink_message at the root of the JSON. Send the flat ApplicationUp in Appendix W - not the wrapped events envelope ({"name":..., "data":{"end_device_ids":...}}) shown in the TTI console. With the wrapper, those keys sit under data, the PHP reads null, and it silently stores nothing while still replying ok.

Template syntax (akenza Handlebars)

  • Use double braces {{ }} - akenza rejects triple braces ("Disallowed: non-encoded text").
  • Do not quote string variables: akenza JSON-encodes {{ }} itself, so write "dev_eui": {{deviceId}} (no surrounding quotes). Quoting produces doubled quotes and breaks matching.
  • {{deviceId}} renders the uppercase DevEUI; {{data.f_port}} / {{data.f_cnt}} are bare numbers; {{data.frm_payload}} is the base64 string.

Verify on webhook.site, then cut over

Point the URL at a webhook.site catcher first. Trigger an uplink and confirm the captured body starts with {"end_device_ids": (not {"name":), that f_cnt is an integer, and frm_payload is clean base64. Then set the URL to https://gw.allmeteo.com/tti/uplink and save.

Verify the rendered body on webhook.site before pointing at gw.allmeteo.com.
Verify the rendered body on webhook.site before pointing at gw.allmeteo.com.

6Verify end to end

Open the station in allMETEO -> Data tab. Measurements arrive every uplink interval with current timestamps, consistent with the akenza decode. The screenshot below shows a MeteoWind landing; the other Gen2 types appear the same way once provisioned.

allMETEO Data tab: live Gen2 frames arriving (example: MeteoWind).
allMETEO Data tab: live Gen2 frames arriving (example: MeteoWind).
Two endpoint quirks worth knowing

gw.allmeteo.com/tti/uplink needs no auth (a bodyless GET returns 200) and always returns 200 ok even on bad input - so akenza's "success" never reflects whether data was stored. The allMETEO Data tab is the only reliable confirmation.

7Troubleshooting

  • Webhook shows success but allMETEO stays empty - the endpoint always returns 200 ok and needs no auth, so akenza's success tells you nothing. The usual cause is a wrapped body: allMETEO reads end_device_ids / uplink_message at the root. Send the unwrapped Appendix W body and confirm dev_eui matches the provisioned station.
  • akenza Data Overview shows no fields, only frm_payload - akenza's DB rejects keys that start with a digit. The appendix decoders prefix them (m_...). If you see data key '..' is invalid, the old decoder is still saved - re-paste with select-all and Save.
  • Decoder edits do not take effect - a partial paste leaves two decodeUplink() / consume() definitions; JavaScript uses the last one. Empty the editor completely before pasting.
  • Frame counter wrong / duplicates - f_cnt is taken from the frame index byte; if a device omits index, f_cnt is left out and allMETEO falls back to 0.
  • Service messages (port 2) are forwarded raw, not decoded - this is intended; allMETEO handles them by dev_eui.
Data Processing Logs - watch the output step for warnings and the webhook result.
Data Processing Logs - watch the output step for warnings and the webhook result.

8Gen2 device catalog

Each device's adapted decoder is in the matching appendix. Payload length is the port-1 periodic frame.

DevicePort-1 payloadKey measurementsDecoder
MeteoHelix IoT Pro Gen216 bytestemp avg/min/max, humidity, irradiation, rainAppendix A1
MeteoWind IoT Pro Gen2 (HNWL)21 bytes8x wind speed (Hz) + 8x direction sub-samplesAppendix A2
MeteoRain IoT Pro Gen26 bytesrain clicks, intensity, intervalAppendix A3
MeteoAG IoT Pro Gen213 bytessoil moisture / EC channels, leaf wetnessAppendix A4
MeteoALTIM IoT Pro Gen216 bytestemperature, humidity, ref + differential pressureAppendix A5
MeteoWind IoT Pro Gen2 / Gen3

The standard MeteoWind Gen2/Gen3 (Dendriform cup, Hz->m/s) is already deployed and has its own dedicated guide; it is the worked example whose screenshots appear throughout. The HNWL variant above is the high-rate logger.

Appendix A1 - MeteoHelix IoT Pro Gen2 decoder

All-in-one weather station: air temperature (average / minimum / maximum), relative humidity, solar irradiation and rainfall. Port-1 periodic frame: 16 bytes. Output fields: temp_avg, temp_min, temp_max, humidity, irradiation (+ irr_min / irr_max), rain_clicks, rain_intens, heater_activated, index. The decodeUplink() below is verbatim from the BARANI repo; the consume() wrapper (Part 4) adds akenza I/O and forwarding. f_cnt is taken from index.

// =============================================================================
// MeteoHelix IoT Pro Gen2 - akenza adapter (BARANI repo decodeUplink + forwarding)
// decodeUplink() is verbatim from barani-design/P_SRC_device_decoders.
// The consume() wrapper adds akenza I/O + allMETEO forwarding (see foot).
// =============================================================================

//MeteoHelix IoT Pro Gen2 Periodic payload decoder
function decodeUplink(input) {
    var bytes = input.bytes;

    var pos = 0;
    var bindata = "";

    var ConvertBase = function (num) {
        return {
            from : function (baseFrom) {
                return {
                    to : function (baseTo) {
                        return parseInt(num, baseFrom).toString(baseTo);
                    }
                };
            }
        };
    };

    function pad(num) {
        var s = "0000000" + num;
        return s.slice(-8);
    }

    ConvertBase.dec2bin = function (num) {
        return pad(ConvertBase(num).from(10).to(2));
    };

    ConvertBase.bin2dec = function (num) {
        return ConvertBase(num).from(2).to(10);
    };

    function data2bits(data) {
        var binary = "";
        for(var i=0; i<data.length; i++) {
            binary += ConvertBase.dec2bin(data[i]);
        }
        return binary;
    }

    function bitShift(bits) {
        var num = ConvertBase.bin2dec(bindata.substr(pos, bits));
        pos += bits;
        return Number(num);
    }

    function precisionRound(number, precision) {
        var factor = Math.pow(10, precision);
        return Math.round(number * factor) / factor;
    }
    
    function batteryIndicator(index, battery_bit, min_value=3.3) {
        var remainder = index % 5;
        
        if ( remainder > 4)
        {
          return 0;
        }
        
        else
        {
          var result = remainder < 5 ? remainder * 0.2 + min_value : remainder * 0.2 + min_value - 1;
          var rounded = Math.round(result * 10) / 10;
          return battery_bit === 1 ? `> ${rounded} V` : ` -- `;
        }
        
    }    

    bindata = data2bits(bytes);

    var index = precisionRound(bitShift(8), 1);

    var battery_bit = bitShift(1);
    var battery = batteryIndicator(index, battery_bit);

    var temp_avg = precisionRound(bitShift(14)*0.01, 2);
    temp_avg = precisionRound(temp_avg + (-50), 2);
    
    //tem_avg = Math.round(temp_avg * 10) / 10;

    var temp_min_diff = precisionRound(bitShift(8)*0.05, 2);
    var temp_max_diff = precisionRound(bitShift(8)*0.05, 2);
    
    var temp_min = temp_avg - temp_min_diff;
    var temp_max = temp_avg + temp_max_diff;
    
    //tem_avg = Math.round(temp_avg * 10) / 10;
    temp_min = Math.round(temp_min * 100) / 100;
    temp_max = Math.round(temp_max * 100) / 100;
    
    var humidity = precisionRound(bitShift(9)*0.2, 2);
    var pressure = precisionRound(bitShift(15)*2.5, 2);
    
    pressure = pressure + 30000;
    
    var irradiation = precisionRound(bitShift(11)*1, 2);
    var irr_min_diff = precisionRound(bitShift(10)*2, 2);
    var irr_max_diff = precisionRound(bitShift(10)*2, 2);
    
    var irr_min = irradiation - irr_min_diff;
    var irr_max = irradiation + irr_max_diff;
    
    var rain_clicks = precisionRound(bitShift(12)*1, 2);
    
    var t_int = precisionRound(bitShift(10)*1, 2);
    
    var time_interval = 0.0;
    
    if ( t_int > 0 )
    {
        time_interval = 728 / t_int;
        time_interval = time_interval * time_interval; 
        time_interval = time_interval.toFixed(3); // 3 decimals	    
    }
    
    var rain_intens = precisionRound(bitShift(10)*0,01, 2);
    
    var heater_activated = precisionRound(bitShift(1)*1, 2);
    
    var alarm_dbg = precisionRound(bitShift(1)*1, 2);
    


    var decoded = {
        "00A_device" : "MeteoHelix IoT Pro Gen2",
        "index": index,
        "battery_bit": battery_bit,
        "battery_indicator": battery,
        "temp_avg": temp_avg,
        "temp_min": temp_min,
        "temp_max": temp_max,
        "humidity": humidity,
        "pressure" : pressure,
        "irradiation": irradiation,
        "irr_min": irr_min,
        "irr_max": irr_max,
        "rain_clicks": rain_clicks,
        "time_interval": time_interval,
        "rain_intens": rain_intens,
        "heater_activated": heater_activated,
        "alarm_dbg": alarm_dbg,
    };

    return {
        data: decoded
    };
}

// ----- akenza wrapper (generic; identical across all Gen2 devices) -----
// Keeps decodeUplink() above verbatim from the BARANI repo. Adds akenza I/O:
// sanitizes display keys for akenza's DB (letter-leading, no spaces) and emits
// frm_payload(base64)+payload_hex+f_port+f_cnt for the allMETEO webhook.
function bytesToHex(bytes) {
  var h = ""; for (var i = 0; i < bytes.length; i++) { var x = (bytes[i] & 0xFF).toString(16); if (x.length < 2) x = "0" + x; h += x; } return h;
}
function bytesToBase64(bytes) {
  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", out = "", i = 0;
  for (i = 0; i + 2 < bytes.length; i += 3) { var n = ((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8)|(bytes[i+2]&0xFF);
    out += chars.charAt((n>>18)&63)+chars.charAt((n>>12)&63)+chars.charAt((n>>6)&63)+chars.charAt(n&63); }
  var rem = bytes.length - i;
  if (rem === 1) { var a=(bytes[i]&0xFF)<<16; out += chars.charAt((a>>18)&63)+chars.charAt((a>>12)&63)+"=="; }
  else if (rem === 2) { var b=((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8); out += chars.charAt((b>>18)&63)+chars.charAt((b>>12)&63)+chars.charAt((b>>6)&63)+"="; }
  return out;
}
function consume(event) {
  var port = event.data.port;
  var bytes = Hex.hexToBytes(event.data.payloadHex);
  var clean = {};
  if (port === 1) {
    var out = decodeUplink({ bytes: bytes, fPort: port });
    var d = (out && out.data) || {};
    for (var k in d) {
      if (!Object.prototype.hasOwnProperty.call(d, k)) continue;
      var nk = String(k).replace(/[^A-Za-z0-9_]/g, "_");
      if (/^[0-9]/.test(nk)) nk = "m_" + nk;
      clean[nk] = d[k];
    }
    if (typeof d.index !== "undefined") clean.f_cnt = d.index;
  }
  clean.frm_payload = bytesToBase64(bytes);
  clean.payload_hex = bytesToHex(bytes);
  clean.f_port = port;
  emit("sample", { data: clean, topic: "default" });
}

Appendix A2 - MeteoWind IoT Pro Gen2 (HNWL) decoder

High-rate wind logger: eight sub-samples of wind speed (Hz) and wind direction per uplink. Port-1 periodic frame: 21 bytes. Output fields: wind_Hz_AVG1..8, wind_DEG_AVG1..8, battery, index. The decodeUplink() below is verbatim from the BARANI repo; the consume() wrapper (Part 4) adds akenza I/O and forwarding. f_cnt is taken from index.

// =============================================================================
// MeteoWindHNWL IoT Pro Gen2 - akenza adapter (BARANI repo decodeUplink + forwarding)
// decodeUplink() is verbatim from barani-design/P_SRC_device_decoders.
// The consume() wrapper adds akenza I/O + allMETEO forwarding (see foot).
// =============================================================================

//MeteoWind_IoT_Pro_Gen2 HNWL periodic payload decoder
function decodeUplink(input) {
    var bytes = input.bytes;

    var pos = 0;
    var bindata = "";

    var anemometer_slope = 0.6335;
    var anemometer_offset = 0.3582;

    var ConvertBase = function (num) {
        return {
            from : function (baseFrom) {
                return {
                    to : function (baseTo) {
                        return parseInt(num, baseFrom).toString(baseTo);
                    }
                };
            }
        };
    };

    function pad(num) {
        var s = "0000000" + num;
        return s.slice(-8);
    }

    ConvertBase.dec2bin = function (num) {
        return pad(ConvertBase(num).from(10).to(2));
    };

    ConvertBase.bin2dec = function (num) {
        return ConvertBase(num).from(2).to(10);
    };

    function data2bits(data) {
        var binary = "";
        for(var i=0; i<data.length; i++) {
            binary += ConvertBase.dec2bin(data[i]);
        }
        return binary;
    }

    function bitShift(bits) {
        var num = ConvertBase.bin2dec(bindata.substr(pos, bits));
        pos += bits;
        return Number(num);
    }

    function precisionRound(number, precision) {
        var factor = Math.pow(10, precision);
        return Math.round(number * factor) / factor;
    }
  

    bindata = data2bits(bytes);

    var index = precisionRound(bitShift(8), 1);

    var batt = precisionRound(bitShift(2)*0.2, 2);
    var battery = precisionRound(batt + 3.5, 2); //3,5V is minimum value
    
    //var battery_bits = bitShift(2);
    
    //var battery = batteryIndicator(index, battery_bit);

    var hz_avg1 = precisionRound(bitShift(10)*0.1, 2);
    var hz_avg2 = precisionRound(bitShift(10)*0.1, 2);
    var hz_avg3 = precisionRound(bitShift(10)*0.1, 2);
    var hz_avg4 = precisionRound(bitShift(10)*0.1, 2);
    var hz_avg5 = precisionRound(bitShift(10)*0.1, 2);
    var hz_avg6 = precisionRound(bitShift(10)*0.1, 2);
    var hz_avg7 = precisionRound(bitShift(10)*0.1, 2);
    var hz_avg8 = precisionRound(bitShift(10)*0.1, 2);

    var deg_1s_avg1 = precisionRound(bitShift(8)*2, 1) ;
    var deg_1s_avg2 = precisionRound(bitShift(8)*2, 1) ;
    var deg_1s_avg3 = precisionRound(bitShift(8)*2, 1) ;
    var deg_1s_avg4 = precisionRound(bitShift(8)*2, 1) ;
    var deg_1s_avg5 = precisionRound(bitShift(8)*2, 1) ;
    var deg_1s_avg6 = precisionRound(bitShift(8)*2, 1) ;
    var deg_1s_avg7 = precisionRound(bitShift(8)*2, 1) ;
    var deg_1s_avg8 = precisionRound(bitShift(8)*2, 1) ;
  
    var debug = precisionRound(bitShift(6)*1, 1);

    var decoded = {
	"00A_device" : "MeteoWind IoT Pro Gen2 HNWL",
        "index": index,
        "battery_bits": batt,
        "battery_indicator": battery,
        "wind_Hz_AVG1": hz_avg1,
	"wind_Hz_AVG2": hz_avg2,
	"wind_Hz_AVG3": hz_avg3,
        "wind_Hz_AVG4": hz_avg4,    
        "wind_Hz_AVG5": hz_avg5,
        "wind_Hz_AVG6": hz_avg6,
        "wind_Hz_AVG7": hz_avg7,
        "wind_Hz_AVG8": hz_avg8,
	"wind_DEG_AVG1": deg_1s_avg1,    
        "wind_DEG_AVG2": deg_1s_avg2,
	"wind_DEG_AVG3": deg_1s_avg3,
	"wind_DEG_AVG4": deg_1s_avg4,
	"wind_DEG_AVG5": deg_1s_avg5,
	"wind_DEG_AVG6": deg_1s_avg6,
	"wind_DEG_AVG7": deg_1s_avg7,
	"wind_DEG_AVG8": deg_1s_avg8,
        "dbg": debug,
    };

    return {
        data: decoded
    };
}


// ----- akenza wrapper (generic; identical across all Gen2 devices) -----
// Keeps decodeUplink() above verbatim from the BARANI repo. Adds akenza I/O:
// sanitizes display keys for akenza's DB (letter-leading, no spaces) and emits
// frm_payload(base64)+payload_hex+f_port+f_cnt for the allMETEO webhook.
function bytesToHex(bytes) {
  var h = ""; for (var i = 0; i < bytes.length; i++) { var x = (bytes[i] & 0xFF).toString(16); if (x.length < 2) x = "0" + x; h += x; } return h;
}
function bytesToBase64(bytes) {
  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", out = "", i = 0;
  for (i = 0; i + 2 < bytes.length; i += 3) { var n = ((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8)|(bytes[i+2]&0xFF);
    out += chars.charAt((n>>18)&63)+chars.charAt((n>>12)&63)+chars.charAt((n>>6)&63)+chars.charAt(n&63); }
  var rem = bytes.length - i;
  if (rem === 1) { var a=(bytes[i]&0xFF)<<16; out += chars.charAt((a>>18)&63)+chars.charAt((a>>12)&63)+"=="; }
  else if (rem === 2) { var b=((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8); out += chars.charAt((b>>18)&63)+chars.charAt((b>>12)&63)+chars.charAt((b>>6)&63)+"="; }
  return out;
}
function consume(event) {
  var port = event.data.port;
  var bytes = Hex.hexToBytes(event.data.payloadHex);
  var clean = {};
  if (port === 1) {
    var out = decodeUplink({ bytes: bytes, fPort: port });
    var d = (out && out.data) || {};
    for (var k in d) {
      if (!Object.prototype.hasOwnProperty.call(d, k)) continue;
      var nk = String(k).replace(/[^A-Za-z0-9_]/g, "_");
      if (/^[0-9]/.test(nk)) nk = "m_" + nk;
      clean[nk] = d[k];
    }
    if (typeof d.index !== "undefined") clean.f_cnt = d.index;
  }
  clean.frm_payload = bytesToBase64(bytes);
  clean.payload_hex = bytesToHex(bytes);
  clean.f_port = port;
  emit("sample", { data: clean, topic: "default" });
}

Appendix A3 - MeteoRain IoT Pro Gen2 decoder

Tipping-bucket rain gauge with rain-intensity correction. Port-1 periodic frame: 6 bytes. Output fields: rain_clicks, rain_intensity_correction, time_btwn, temp-above-2-deg, battery, index. The decodeUplink() below is verbatim from the BARANI repo; the consume() wrapper (Part 4) adds akenza I/O and forwarding. f_cnt is taken from index.

// =============================================================================
// MeteoRain IoT Pro Gen2 - akenza adapter (BARANI repo decodeUplink + forwarding)
// decodeUplink() is verbatim from barani-design/P_SRC_device_decoders.
// The consume() wrapper adds akenza I/O + allMETEO forwarding (see foot).
// =============================================================================

//MeteoRain IoT Pro Gen2 periodic payload decoder
function decodeUplink(input) {
    var bytes = input.bytes;

    var pos = 0;
    var bindata = "";

    var ConvertBase = function (num) {
        return {
            from : function (baseFrom) {
                return {
                    to : function (baseTo) {
                        return parseInt(num, baseFrom).toString(baseTo);
                    }
                };
            }
        };
    };

    function pad(num) {
        var s = "0000000" + num;
        return s.slice(-8);
    }

    ConvertBase.dec2bin = function (num) {
        return pad(ConvertBase(num).from(10).to(2));
    };

    ConvertBase.bin2dec = function (num) {
        return ConvertBase(num).from(2).to(10);
    };

    function data2bits(data) {
        var binary = "";
        for(var i=0; i<data.length; i++) {
            binary += ConvertBase.dec2bin(data[i]);
        }
        return binary;
    }

    function bitShift(bits) {
        var num = ConvertBase.bin2dec(bindata.substr(pos, bits));
        pos += bits;
        return Number(num);
    }

    function precisionRound(number, precision) {
        var factor = Math.pow(10, precision);
        return Math.round(number * factor) / factor;
    }
    
    function batteryIndicator(index, battery_bit, min_value=3.3) {
        var remainder = index % 5;

        if ( remainder > 4)
        {
          return 0;
        }

        var result = remainder < 5 ? remainder * 0.2 + min_value : remainder * 0.2 + min_value - 1;       
        var rounded = Math.round(result * 10) / 10;
        return battery_bit === 1 ? `> ${rounded} V` : ` -- `;
    }    

    bindata = data2bits(bytes);

    var index = precisionRound(bitShift(8), 1);
    var battery_bit = bitShift(1);
    var battery = batteryIndicator(index, battery_bit);
    
    var rain_clicks = precisionRound(bitShift(12)*1, 2);
    var tim = precisionRound(bitShift(10)*1, 2);
    
    var time_bt = 0.0;

    if ( tim > 0 )
    {
      time_bt = ( 728 / tim );
      time_bt = time_bt * time_bt;
      time_bt = time_bt.toFixed(3); // 3 decimals
    }
  
    //var time_bt = ( 728 / tim );
    //time_bt = time_bt * time_bt;
    
    var rain_intensity = precisionRound(bitShift(12)*0.01, 2);
    var temp = precisionRound(bitShift(1)*1, 2);
    var debug = precisionRound(bitShift(4)*1, 2);


    var decoded = {
        "00A_device" : "MeteoRain IoT Pro Gen2",
        "index": index,
        "battery_bit": battery_bit,
        "battery_indicator": battery,
        "rain_clicks": rain_clicks,
        "time_btwn": time_bt,
        "rain_intensity_correction": rain_intensity,
        "temp above 2 deg": temp,
        "debug": debug,
    };

    return {
        data: decoded
    };
}

// ----- akenza wrapper (generic; identical across all Gen2 devices) -----
// Keeps decodeUplink() above verbatim from the BARANI repo. Adds akenza I/O:
// sanitizes display keys for akenza's DB (letter-leading, no spaces) and emits
// frm_payload(base64)+payload_hex+f_port+f_cnt for the allMETEO webhook.
function bytesToHex(bytes) {
  var h = ""; for (var i = 0; i < bytes.length; i++) { var x = (bytes[i] & 0xFF).toString(16); if (x.length < 2) x = "0" + x; h += x; } return h;
}
function bytesToBase64(bytes) {
  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", out = "", i = 0;
  for (i = 0; i + 2 < bytes.length; i += 3) { var n = ((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8)|(bytes[i+2]&0xFF);
    out += chars.charAt((n>>18)&63)+chars.charAt((n>>12)&63)+chars.charAt((n>>6)&63)+chars.charAt(n&63); }
  var rem = bytes.length - i;
  if (rem === 1) { var a=(bytes[i]&0xFF)<<16; out += chars.charAt((a>>18)&63)+chars.charAt((a>>12)&63)+"=="; }
  else if (rem === 2) { var b=((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8); out += chars.charAt((b>>18)&63)+chars.charAt((b>>12)&63)+chars.charAt((b>>6)&63)+"="; }
  return out;
}
function consume(event) {
  var port = event.data.port;
  var bytes = Hex.hexToBytes(event.data.payloadHex);
  var clean = {};
  if (port === 1) {
    var out = decodeUplink({ bytes: bytes, fPort: port });
    var d = (out && out.data) || {};
    for (var k in d) {
      if (!Object.prototype.hasOwnProperty.call(d, k)) continue;
      var nk = String(k).replace(/[^A-Za-z0-9_]/g, "_");
      if (/^[0-9]/.test(nk)) nk = "m_" + nk;
      clean[nk] = d[k];
    }
    if (typeof d.index !== "undefined") clean.f_cnt = d.index;
  }
  clean.frm_payload = bytesToBase64(bytes);
  clean.payload_hex = bytesToHex(bytes);
  clean.f_port = port;
  emit("sample", { data: clean, topic: "default" });
}

Appendix A4 - MeteoAG IoT Pro Gen2 decoder

Agricultural soil probe: soil moisture / electrical conductivity channels plus leaf-wetness and temperature selectors. Port-1 periodic frame: 13 bytes. Output fields: soil_e1..3, soil_f1..3, soil_g3, leaf_select, temp_select, soil_select, battery, index. The decodeUplink() below is verbatim from the BARANI repo; the consume() wrapper (Part 4) adds akenza I/O and forwarding. f_cnt is taken from index.

// =============================================================================
// MeteoAG IoT Pro Gen2 - akenza adapter (BARANI repo decodeUplink + forwarding)
// decodeUplink() is verbatim from barani-design/P_SRC_device_decoders.
// The consume() wrapper adds akenza I/O + allMETEO forwarding (see foot).
// =============================================================================

// MeteoAG IoT Pro Gen2 periodic payload decoder for v1.02.002 +
function decodeUplink(input) {
    var bytes = input.bytes;

    var pos = 0;
    var bindata = "";

    var ConvertBase = function (num) {
        return {
            from : function (baseFrom) {
                return {
                    to : function (baseTo) {
                        return parseInt(num, baseFrom).toString(baseTo);
                    }
                };
            }
        };
    };

    function pad(num) {
        var s = "0000000" + num;
        return s.slice(-8);
    }

    ConvertBase.dec2bin = function (num) {
        return pad(ConvertBase(num).from(10).to(2));
    };

    ConvertBase.bin2dec = function (num) {
        return ConvertBase(num).from(2).to(10);
    };

    function data2bits(data) {
        var binary = "";
        for(var i=0; i<data.length; i++) {
            binary += ConvertBase.dec2bin(data[i]);
        }
        return binary;
    }

    function bitShift(bits) {
        var num = ConvertBase.bin2dec(bindata.substr(pos, bits));
        pos += bits;
        return Number(num);
    }

    function precisionRound(number, precision) {
        var factor = Math.pow(10, precision);
        return Math.round(number * factor) / factor;
    }
    
    function batteryIndicator(index, battery_bit, min_value=3.3) {
        var remainder = index % 5;
        
        if ( remainder > 4)
        {
          return 0;
        }
        
        else
        {
          var result = remainder < 5 ? remainder * 0.2 + min_value : remainder * 0.2 + min_value - 1;
          var rounded = Math.round(result * 10) / 10;
          return battery_bit === 1 ? `> ${rounded} V` : ` -- `;
        }
        
    }    

    bindata = data2bits(bytes);

    var index = precisionRound(bitShift(8), 1);

    var battery_bit = bitShift(1);
    var battery = batteryIndicator(index, battery_bit);

    var soil_select = precisionRound(bitShift(3)*1, 2);
    var temp_select = precisionRound(bitShift(3)*1, 2);
    var leaf_select = precisionRound(bitShift(3)*1, 2); 
    
    var soil_e1 = precisionRound(bitShift(12)*1, 2);  
    var soil_e2 = precisionRound(bitShift(12)*1, 2);  
    var soil_e3 = precisionRound(bitShift(12)*1, 2);  

    var soil_f1 = precisionRound(bitShift(12)*1, 2);  
    var soil_f2 = precisionRound(bitShift(12)*1, 2);  
    var soil_f3 = precisionRound(bitShift(12)*1, 2);    
  
    var soil_g3 = precisionRound(bitShift(12)*1, 2); 
    
    var dbg = precisionRound(bitShift(2)*1, 2);
    


    var decoded = {
	"00A_device" : "MeteoAG IoT Pro Gen2",
        "index": index,
        "battery_bit": battery_bit,
        "battery_indicator": battery,
        "soil_select": soil_select,
        "temp_select": temp_select,
        "leaf_select": leaf_select,
        "soil_e1": soil_e1,
	"soil_e2": soil_e2,
	"soil_e3": soil_e3,
        "soil_f1": soil_f1,
	"soil_f2": soil_f2,
	"soil_f3": soil_f3,
	"soil_g3": soil_g3,	    
        "dbg": dbg,
    };

    return {
        data: decoded
    };
}
// ----- akenza wrapper (generic; identical across all Gen2 devices) -----
// Keeps decodeUplink() above verbatim from the BARANI repo. Adds akenza I/O:
// sanitizes display keys for akenza's DB (letter-leading, no spaces) and emits
// frm_payload(base64)+payload_hex+f_port+f_cnt for the allMETEO webhook.
function bytesToHex(bytes) {
  var h = ""; for (var i = 0; i < bytes.length; i++) { var x = (bytes[i] & 0xFF).toString(16); if (x.length < 2) x = "0" + x; h += x; } return h;
}
function bytesToBase64(bytes) {
  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", out = "", i = 0;
  for (i = 0; i + 2 < bytes.length; i += 3) { var n = ((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8)|(bytes[i+2]&0xFF);
    out += chars.charAt((n>>18)&63)+chars.charAt((n>>12)&63)+chars.charAt((n>>6)&63)+chars.charAt(n&63); }
  var rem = bytes.length - i;
  if (rem === 1) { var a=(bytes[i]&0xFF)<<16; out += chars.charAt((a>>18)&63)+chars.charAt((a>>12)&63)+"=="; }
  else if (rem === 2) { var b=((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8); out += chars.charAt((b>>18)&63)+chars.charAt((b>>12)&63)+chars.charAt((b>>6)&63)+"="; }
  return out;
}
function consume(event) {
  var port = event.data.port;
  var bytes = Hex.hexToBytes(event.data.payloadHex);
  var clean = {};
  if (port === 1) {
    var out = decodeUplink({ bytes: bytes, fPort: port });
    var d = (out && out.data) || {};
    for (var k in d) {
      if (!Object.prototype.hasOwnProperty.call(d, k)) continue;
      var nk = String(k).replace(/[^A-Za-z0-9_]/g, "_");
      if (/^[0-9]/.test(nk)) nk = "m_" + nk;
      clean[nk] = d[k];
    }
    if (typeof d.index !== "undefined") clean.f_cnt = d.index;
  }
  clean.frm_payload = bytesToBase64(bytes);
  clean.payload_hex = bytesToHex(bytes);
  clean.f_port = port;
  emit("sample", { data: clean, topic: "default" });
}

Appendix A5 - MeteoALTIM IoT Pro Gen2 decoder

Barometric and differential-pressure sensor (altimetry / flow). Port-1 periodic frame: 16 bytes. Output fields: temperature, humidity, ref_p, diff_p0..5, std_dev0..5, battery, index. The decodeUplink() below is verbatim from the BARANI repo; the consume() wrapper (Part 4) adds akenza I/O and forwarding. f_cnt is taken from index.

// =============================================================================
// MeteoALTIM IoT Pro Gen2 - akenza adapter (BARANI repo decodeUplink + forwarding)
// decodeUplink() is verbatim from barani-design/P_SRC_device_decoders.
// The consume() wrapper adds akenza I/O + allMETEO forwarding (see foot).
// =============================================================================

function decodeUplink(input) {
    var bytes = input.bytes;

    var pos = 0;
    var bindata = "";

    var ConvertBase = function (num) {
        return {
            from : function (baseFrom) {
                return {
                    to : function (baseTo) {
                        return parseInt(num, baseFrom).toString(baseTo);
                    }
                };
            }
        };
    };

    function pad(num) {
        var s = "0000000" + num;
        return s.slice(-8);
    }

    ConvertBase.dec2bin = function (num) {
        return pad(ConvertBase(num).from(10).to(2));
    };

    ConvertBase.bin2dec = function (num) {
        return ConvertBase(num).from(2).to(10);
    };

    function data2bits(data) {
        var binary = "";
        for(var i=0; i<data.length; i++) {
            binary += ConvertBase.dec2bin(data[i]);
        }
        return binary;
    }

    function bitShift(bits) {
        var num = ConvertBase.bin2dec(bindata.substr(pos, bits));
        pos += bits;
        return Number(num);
    }

    function precisionRound(number, precision) {
        var factor = Math.pow(10, precision);
        return Math.round(number * factor) / factor;
    }

    function batteryIndicator(index, battery_bit, min_value=3.3) {
        var remainder = index % 10;
        var result = remainder < 5 ? remainder * 0.2 + min_value : remainder * 0.2 + min_value - 1;

        var rounded = Math.round(result * 10) / 10;
        return battery_bit === 1 ? `> ${rounded} V` : ` -- `;
    }

    bindata = data2bits(bytes);

    var index = precisionRound(bitShift(8), 1);

    var battery_bit = bitShift(1);
    var battery = batteryIndicator(index, battery_bit);

    var temperature = precisionRound(bitShift(6)*2, 1) - 45;
    var humidity = precisionRound(bitShift(4)*4, 1) + 40;
    var ref_p = ((precisionRound(bitShift(13)*10, 1)) + 30000);
    var diff_p0 = precisionRound(bitShift(10)*0.5, 1) - 256;
    var diff_p1 = precisionRound(bitShift(10)*0.5, 1)- 256;
    var diff_p2 = precisionRound(bitShift(10)*0.5, 1)- 256;
    var diff_p3 = precisionRound(bitShift(10)*0.5, 1)- 256;
    var diff_p4 = precisionRound(bitShift(10)*0.5, 1)- 256;
    var diff_p5 = precisionRound(bitShift(10)*0.5, 1)- 256;
    var std_dev0 = precisionRound(bitShift(6), 1);
    var std_dev1 = precisionRound(bitShift(6), 1);
    var std_dev2 = precisionRound(bitShift(6), 1);
    var std_dev3 = precisionRound(bitShift(6), 1);
    var std_dev4 = precisionRound(bitShift(6), 1);
    var std_dev5 = precisionRound(bitShift(6), 1);

    function toHexString(byteArray) {
        return Array.from(byteArray, function(byte) {
            return ('0' + (byte & 0xFF).toString(16)).slice(-2);
        }).join('')
    }

    var st = toHexString(input.bytes).toUpperCase();

    var decoded = {
        "00A_device" : "MeteoAltim IoT Pro Gen2",
        "payload" : st,
        "index": index,
        "battery_bit": battery_bit,
        "battery_indicator": battery,
        "temperature": temperature,
        "humidity": humidity,
        "ref_p": ref_p,
        "diff_p0":diff_p0,
        "diff_p1":diff_p1,
        "diff_p2":diff_p2,
        "diff_p3":diff_p3,
        "diff_p4":diff_p4,
        "diff_p5":diff_p5,
        "std_dev0":std_dev0,
        "std_dev1":std_dev1,
        "std_dev2":std_dev2,
        "std_dev3":std_dev3,
        "std_dev4":std_dev4,
        "std_dev5":std_dev5,
    };

    return {
        data: decoded
    };
}
// ----- akenza wrapper (generic; identical across all Gen2 devices) -----
// Keeps decodeUplink() above verbatim from the BARANI repo. Adds akenza I/O:
// sanitizes display keys for akenza's DB (letter-leading, no spaces) and emits
// frm_payload(base64)+payload_hex+f_port+f_cnt for the allMETEO webhook.
function bytesToHex(bytes) {
  var h = ""; for (var i = 0; i < bytes.length; i++) { var x = (bytes[i] & 0xFF).toString(16); if (x.length < 2) x = "0" + x; h += x; } return h;
}
function bytesToBase64(bytes) {
  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", out = "", i = 0;
  for (i = 0; i + 2 < bytes.length; i += 3) { var n = ((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8)|(bytes[i+2]&0xFF);
    out += chars.charAt((n>>18)&63)+chars.charAt((n>>12)&63)+chars.charAt((n>>6)&63)+chars.charAt(n&63); }
  var rem = bytes.length - i;
  if (rem === 1) { var a=(bytes[i]&0xFF)<<16; out += chars.charAt((a>>18)&63)+chars.charAt((a>>12)&63)+"=="; }
  else if (rem === 2) { var b=((bytes[i]&0xFF)<<16)|((bytes[i+1]&0xFF)<<8); out += chars.charAt((b>>18)&63)+chars.charAt((b>>12)&63)+chars.charAt((b>>6)&63)+"="; }
  return out;
}
function consume(event) {
  var port = event.data.port;
  var bytes = Hex.hexToBytes(event.data.payloadHex);
  var clean = {};
  if (port === 1) {
    var out = decodeUplink({ bytes: bytes, fPort: port });
    var d = (out && out.data) || {};
    for (var k in d) {
      if (!Object.prototype.hasOwnProperty.call(d, k)) continue;
      var nk = String(k).replace(/[^A-Za-z0-9_]/g, "_");
      if (/^[0-9]/.test(nk)) nk = "m_" + nk;
      clean[nk] = d[k];
    }
    if (typeof d.index !== "undefined") clean.f_cnt = d.index;
  }
  clean.frm_payload = bytesToBase64(bytes);
  clean.payload_hex = bytesToHex(bytes);
  clean.f_port = port;
  emit("sample", { data: clean, topic: "default" });
}

Appendix W - webhook custom payload (unwrapped TTN v3)

Paste this into the akenza Webhook connector's Custom payload. It is the unwrapped ApplicationUp that allMETEO's tti/uplink.php reads ($data['end_device_ids']['dev_eui'], $data['uplink_message']['frm_payload'], ['f_port'], ['f_cnt'], ['received_at'] - all at the root). Set device_id to the station serial; dev_eui comes from akenza's {{deviceId}}. application_id live-2020-10 tags the device with the same source as the TTI fleet.

{
  "end_device_ids": {
    "device_id": "<station-serial>",
    "application_ids": { "application_id": "live-2020-10" },
    "dev_eui": {{deviceId}},
    "dev_addr": "2700BE60"
  },
  "uplink_message": {
    "f_port": {{data.f_port}},
    "f_cnt": {{data.f_cnt}},
    "frm_payload": {{data.frm_payload}},
    "received_at": {{timestamp}},
    "rx_metadata": [
      { "gateway_ids": { "eui": "0000000000000000" }, "rssi": -80, "snr": 8.0 }
    ],
    "settings": {
      "data_rate": { "lora": { "spreading_factor": 7, "bandwidth": 125000 } }
    }
  }
}

Appendix O - changing keys over the air (Port 100)

Gen2 devices accept an OTAA credential change on downlink port 100: a 24-byte payload = [AppKey 16 bytes][AppEUI 8 bytes] (48 hex characters), sent as a confirmed downlink. To change only the AppEUI, keep the current AppKey and append the new AppEUI.

  • Register the new keys on the join server (akenza / Swisscom on CaaS) before the device next rejoins, or it goes dark.
  • Send via akenza Device -> Downlink -> Raw -> Payload HEX -> Port 100 -> Confirmed.
  • Byte order is as displayed (MSB-first); confirm against firmware before sending.