BARANI MeteoX IoT Pro Gen2 -> allMETEO via akenza (Master 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.
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.
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 thefrm_payload. - The device's adapted decoder from the appendix for its 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).
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_devicebecomesm_00A_device.) - It appends
frm_payload(base64),payload_hex,f_portandf_cnt(from the frameindex). 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_cntappear, then Save.
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.
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.
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.
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 okand needs no auth, so akenza's success tells you nothing. The usual cause is a wrapped body: allMETEO readsend_device_ids/uplink_messageat the root. Send the unwrapped Appendix W body and confirmdev_euimatches 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 seedata 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_cntis taken from the frameindexbyte; if a device omitsindex,f_cntis 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.
8Gen2 device catalog
Each device's adapted decoder is in the matching appendix. Payload length is the port-1 periodic frame.
| Device | Port-1 payload | Key measurements | Decoder |
|---|---|---|---|
| MeteoHelix IoT Pro Gen2 | 16 bytes | temp avg/min/max, humidity, irradiation, rain | Appendix A1 |
| MeteoWind IoT Pro Gen2 (HNWL) | 21 bytes | 8x wind speed (Hz) + 8x direction sub-samples | Appendix A2 |
| MeteoRain IoT Pro Gen2 | 6 bytes | rain clicks, intensity, interval | Appendix A3 |
| MeteoAG IoT Pro Gen2 | 13 bytes | soil moisture / EC channels, leaf wetness | Appendix A4 |
| MeteoALTIM IoT Pro Gen2 | 16 bytes | temperature, humidity, ref + differential pressure | Appendix A5 |
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.
