MeteoWind Gen2 -> allMETEO via akenza - Integration Guide

MeteoWind Gen2 -> allMETEO via akenza - Integration Guide

BARANI DESIGN · Integration Guide

MeteoWind IoT Pro Gen2 → allMETEO, via akenza

Connect a Swisscom-network MeteoWind Gen2 wind sensor to the allMETEO platform: decode the Gen2 payload in akenza and forward it in The Things Industries (TTN v3) format.

Prepared 23 June 2026 · Reference unit SN 2605LW044 / DevEUI AC1F09FFFE0E18D8

Verified working end to end. A MeteoWind Gen2 on Swisscom was decoded in akenza and forwarded to gw.allmeteo.com/tti/uplink; wind data arrives in the allMETEO Data tab (see Part 6). The webhook body must be the unwrapped TTN-v3 ApplicationUp (Appendix B) — the wrapped events envelope does not work.

This guide connects a BARANI MeteoWind IoT Pro Gen2 wind sensor - running on the Swisscom LoRaWAN network through akenza - so its measurements flow into the allMETEO platform and appear as just another station alongside the rest of the fleet. It covers device registration, the akenza decoder for the Gen2 14-byte message format, an optional wind-speed-in-m/s conversion, registering the station in allMETEO, and forwarding the data in The Things Industries (TTN v3) webhook format.

Architecture

Every uplink takes the same path. The sensor talks LoRaWAN to Swisscom; akenza receives it, decodes it, and forwards a copy to allMETEO in the format allMETEO already understands.

Sensor  (MeteoWind IoT Pro Gen2)
   |   LoRaWAN uplink - FPort 1 - 14-byte payload
   v
Swisscom LoRaWAN  -->  akenza   (Swisscom CaaS device connector)
                          |   Device Type decodes the payload
                          |   Webhook output connector builds a TTN-v3 body
                          v
                       gw.allmeteo.com/tti/uplink  -->  allMETEO station (matched by DevEUI)

For context, this is how the existing fleet already reaches allMETEO - the Swisscom routing posts each uplink straight to gw.allmeteo.com (port 1 to the /tti/uplink endpoint):

How the fleet reaches allMETEO: Swisscom ThingPark AS routing posts uplinks to gw.allmeteo.com (port 1 -> /tti/uplink).
How the fleet reaches allMETEO: Swisscom ThingPark AS routing posts uplinks to gw.allmeteo.com (port 1 -> /tti/uplink).

Before you start

  • An akenza workspace with the sensor added on Swisscom LoRaWAN (CaaS).
  • An allMETEO account where the station will live.
  • The device DevEUI (e.g. AC1F09FFFE0E18D8) and confirmation it sends wind data on FPort 1.
  • A unit running Gen2 firmware - this guide targets the 14-byte Gen2 periodic payload.

1Register the device in akenza

Add the sensor in your akenza workspace using the Swisscom LoRaWAN connectivity. Once it joins, open the device and confirm its identity on the Connectivity details tab - you need the DevEUI, and you should see uplinks arriving on FPort 1.

akenza workspace — the MeteoWind registered under the Swisscom CaaS connectivity.
akenza workspace — the MeteoWind registered under the Swisscom CaaS connectivity.
Device -> Connectivity details: DevEUI AC1F09FFFE0E18D8, OTAA, Class A, data on FPort 1.
Device -> Connectivity details: DevEUI AC1F09FFFE0E18D8, OTAA, Class A, data on FPort 1.
Which port carries what

Wind data is on FPort 1. FPort 2 carries service messages and FPort 3 carries alarms - different formats, decoded separately. The decoder below only wind-decodes port 1.

2Decode the Gen2 payload in akenza

akenza decodes payloads on the Device Type, not on the device - so there is no decoder field in the device view. The Gen2 sensor sends a 14-byte periodic payload; an older 10-byte decoder rejects every frame, which looks like this:

The symptom: every uplink rejected with '10 bytes are required' - the old 10-byte decoder against a 14-byte Gen2 frame.
The symptom: every uplink rejected with '10 bytes are required' - the old 10-byte decoder against a 14-byte Gen2 frame.

Open Device details -> [your device type] -> Uplink -> Open script editor, replace the whole script with the decoder in Appendix A, then test before saving.

Device Type -> Uplink -> Open script editor. The payload decoder lives on the Device Type, not on the device.
Device Type -> Uplink -> Open script editor. The payload decoder lives on the Device Type, not on the device.

In the editor's Script arguments, set Payload HEX to a real captured frame and Port to 1, click Run script, and confirm Emitted Samples shows sane wind values (see the reference vectors in Appendix C).

After pasting the Gen2 decoder: clean wind values (Hz_avg, direction, gust) in Emitted Samples.
After pasting the Gen2 decoder: clean wind values (Hz_avg, direction, gust) in Emitted Samples.
Save, don't just run

Click Save after the test - running the script only previews it. Until you save, live uplinks keep using the old decoder.

3Wind speed in m/s

The MeteoWind reports anemometer rotation frequency in Hz. allMETEO's portal converts it with a quadratic linearization v = A·f² + B·f + C. For the Dendriform Elliptic cup (from 2023) the default constants are:

Wind speed (m/s) = -0.00065 * Hz^2 + 0.67500 * Hz + 0.20000      (accuracy +/- 0.2 m/s)

The decoder in Appendix A applies this and emits 04b_Wind_avg_ms, 05b_Wind_3s_gust_ms, 06b_Wind_1s_gust_ms, and 07b_Wind_3s_min_ms next to the Hz fields:

// allMETEO "Quadratic linearization": v = A*f^2 + B*f + C   (accuracy +/-0.2 m/s)
// Dendriform Elliptic cup default constants. NOTE: the quadratic term is NEGATIVE.
var WIND_A = -0.00065;
var WIND_B =  0.67500;
var WIND_C =  0.20000;
function windSpeedMs(hz) { return precisionRound(WIND_A * hz * hz + WIND_B * hz + WIND_C, 2); }

// emitted in the FPort-1 return object, next to the Hz fields:
//   "04b_Wind_avg_ms":     windSpeedMs(Hz_avg),
//   "05b_Wind_3s_gust_ms": windSpeedMs(Hz_3s_gust),
//   "06b_Wind_1s_gust_ms": windSpeedMs(Hz_1s_gust),
//   "07b_Wind_3s_min_ms":  windSpeedMs(Hz_3s_min),
Mind the sign

The quadratic term is negative (-0.00065). A positive value reads wind high — about +3 m/s at 50 Hz. The constant C = 0.2 also means Hz = 0 maps to 0.20 m/s, not zero.

No double counting

allMETEO applies its own cup calibration to the raw payload it receives (Part 5), driven by the cup you pick in its settings. The m/s here is a convenience for the akenza side - it does not change what allMETEO stores.

4Register the station in allMETEO

In allMETEO, register or confirm the station keyed on the device DevEUI (shown lowercase in the UI, e.g. ac1f09fffe0e18d8). Under MeteoWind Settings, select the correct anemometer cup - for this unit, Dendriform Elliptic (from 2023) - so allMETEO applies the right wind-speed calibration.

allMETEO: the station registered by DevEUI, with the Dendriform Elliptic Anemometer Cup (from 2023) selected.
allMETEO: the station registered by DevEUI, with the Dendriform Elliptic Anemometer Cup (from 2023) selected.
Matching is by DevEUI

allMETEO matches incoming uplinks to this station by DevEUI, so the value akenza forwards as dev_eui must match the station's DevEUI.

5Forward the data to allMETEO (TTN v3)

allMETEO ingests through gw.allmeteo.com/tti/uplink, which expects The Things Industries (TTN v3) webhook JSON, with the payload base64-encoded in frm_payload. We make akenza emit that shape with a Webhook output connector and a custom payload template.

5.1 Emit the raw payload from the decoder

akenza hands the script only payloadHex and port, so the decoder in Appendix A also emits frm_payload (the payload, base64-encoded) and f_port - the fields the template forwards. No external base64 library is needed; the encoder is built in.

5.2 Add the webhook connector

Go to Data Flows -> Allmeteo -> Add connector -> Webhook. Set Method POST, Content type JSON, Auth type None, Mode Event.

Add a Webhook output connector to the Allmeteo data flow.
Add a Webhook output connector to the Allmeteo data flow.
The data flow with the allMETEO webhook running alongside the akenza database output.
The data flow with the allMETEO webhook running alongside the akenza database output.

5.3 Shape the body (TTN v3 template)

Toggle Use custom payload on, clear the auto-filled template, and paste the template from Appendix B.

Use custom payload -> the TTN-v3 template that shapes the outgoing body.
Use custom payload -> the TTN-v3 template that shapes the outgoing body.
Template gotchas (learned the hard way)

Use double braces {{ }} - akenza rejects triple braces with "Disallowed: non-encoded text in templates". Do not put quotes around string variables: akenza JSON-encodes them itself, so "dev_eui": "{{deviceId}}" yields doubled quotes (\"AC1F...\") and breaks matching - write "dev_eui": {{deviceId}} instead. f_port/f_cnt come out as bare numbers; base64 padding passes through fine.

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 B - 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. The flat body is shorter, and it is what the fleet actually sends.

5.4 Verify on webhook.site, then cut over

Point the connector 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 connector + Save Data Flow.

Verify the rendered body on webhook.site before pointing the connector at gw.allmeteo.com/tti/uplink.
Verify the rendered body on webhook.site before pointing the connector at gw.allmeteo.com/tti/uplink.
Confirmed working

With the unwrapped body and the URL set to gw.allmeteo.com/tti/uplink, allMETEO ingests the device. Two endpoint quirks that cost real debugging time: it requires no auth (a bodyless GET returns 200), and it always returns 200 ok even on bad input - so akenza's "success" never reflected whether anything was stored. The only reliable confirmation is the allMETEO Data tab (Part 6).

6Verify end to end

Open the station in allMETEO -> Data tab. Wind measurements arrive every ~10 minutes with current timestamps - battery voltage, wind average / maximum / minimum (knots), and wind direction (degrees) - consistent with the akenza decode.

allMETEO Data tab: live MeteoWind Gen2 frames arriving - battery, wind avg/max/min (knots), wind direction (degrees).
allMETEO Data tab: live MeteoWind Gen2 frames arriving - battery, wind avg/max/min (knots), wind direction (degrees).
End to end confirmed

Live MeteoWind Gen2 frames are landing in allMETEO via akenza. The full path - sensor -> Swisscom LoRaWAN -> akenza (decode + webhook) -> gw.allmeteo.com/tti/uplink -> allMETEO storage - is operational.

Troubleshooting

  • "10 / 14 bytes required" on every frame - wrong decoder version. A 14-byte Gen2 unit needs the Gen2 decoder; a 10-byte decoder rejects it.
  • No data after editing the decoder - you ran but did not Save. For the webhook, you also need Save connector *and* Save Data Flow.
  • frm_payload arrives as ...=, or values come out double-quoted (\"AC1F...\") - you wrapped a variable in quotes. akenza JSON-encodes {{ }} itself, so write {{data.frm_payload}} (no surrounding quotes), not "{{data.frm_payload}}". Do not use triple braces - akenza rejects them with "Disallowed: non-encoded text in templates".
  • Webhook shows success but allMETEO stays empty - the endpoint always returns 200 ok (even on bad input) 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, so if they are nested under data it stores nothing. Send the unwrapped Appendix B body and confirm dev_eui matches the provisioned station.
  • akenza Data Overview shows no wind, only frm_payload - akenza's database rejects measurement keys that start with a digit (01_Index...). The Appendix A decoder prefixes them (m01_Index...). If you see data key '01_...' is invalid warnings in Logs, the old decoder is still saved - replace it and Save (select-all first; a partial paste leaves two Decoder() and the old one wins).
  • Device looks "offline" but logs show errors - it is transmitting; the errors are the decoder rejecting frames. Check Message Logs for the real story.
  • webhook.site stops receiving - the free tier caps at 50 requests and the URL expires after 7 days. Grab a fresh URL and update the connector.

Appendix A - akenza uplink decoder

Paste this into the Device Type's Uplink script editor (select all and replace any existing script, then Save - a partial paste leaves two Decoder() definitions and the old one wins). Wind decode is verified against the BARANI Gen2 reference parser. It also emits frm_payload (base64) + f_port + f_cnt for the webhook, and prefixes display keys with m (m01_Index...) because akenza's database rejects keys that start with a digit.

// =============================================================================
// MeteoWind IoT Pro Gen2 - akenza uplink decoder + TTN-forward fields + m/s  (v5)
// Periodic wind payload (FPort 1): 14 bytes / 112 bits.
// Decodes wind for akenza display, converts Hz -> m/s, and emits frm_payload
// (base64) + f_port so the Webhook output connector can build a TTN-v3 body.
// v5: wind display keys are letter-prefixed ('m01_'...) because akenza's DB
// rejects keys starting with a digit. Forwarding keys (frm_payload/f_port/
// f_cnt/payload_hex) are unchanged - allMETEO is unaffected by this rename.
// for allMETEO /tti/uplink. Wind decode verified vs the BARANI Gen2 parser.
// =============================================================================

var bindata = "";
var pos = 0;

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 b = ""; for (var i = 0; i < data.length; i++) { b += ConvertBase.dec2bin(data[i]); } return b; }
function bitShift(bits) { var num = ConvertBase.bin2dec(bindata.substr(pos, bits)); pos += bits; return Number(num); }
function precisionRound(number, precision) { var f = Math.pow(10, precision); return Math.round(number * f) / f; }

// --- self-contained helpers for forwarding (no btoa / Buffer dependency) ---
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+/";
  var out = "";
  var 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;
}

// --- Cup calibration: rotation frequency (Hz) -> wind speed (m/s) ---
// allMETEO "Quadratic linearization": v = A*f^2 + B*f + C   (accuracy +/-0.2 m/s)
// Dendriform Elliptic cup default constants. NOTE the quadratic term is NEGATIVE.
var WIND_A = -0.00065;
var WIND_B =  0.67500;
var WIND_C =  0.20000;
function windSpeedMs(hz) { return precisionRound(WIND_A * hz * hz + WIND_B * hz + WIND_C, 2); }

function Decoder(bytes, port) {
  // Raw-payload forwarding fields, computed for every frame (any port / length).
  var fwd_payload_hex = bytesToHex(bytes);
  var fwd_frm_payload = bytesToBase64(bytes);

  // FPort 1 = periodic wind data. 2 = service, 3 = alarm -> raw forwarded, not wind-decoded here.
  if (port !== 1) {
    return {
      "message_type": (port === 2 ? "service" : (port === 3 ? "alarm" : "unknown")),
      "f_port": port,
      "frm_payload": fwd_frm_payload,
      "payload_hex": fwd_payload_hex
    };
  }

  if (bytes.length != 14) {
    return {
      "status": "ERROR",
      "description": "14 bytes are required (Gen2 periodic)",
      "f_port": port,
      "frm_payload": fwd_frm_payload,
      "payload_hex": fwd_payload_hex
    };
  }

  pos = 0;                      // reset cursor every uplink (runtime context may persist)
  bindata = data2bits(bytes);   // assign the GLOBAL bindata (do NOT use 'let' - shadowing -> NaN)

  var Index        = bitShift(8);
  var BattState    = bitShift(1);
  var Hz_avg       = precisionRound(bitShift(12) * 0.02, 2);
  var Hz_3s_gust   = precisionRound(Hz_avg + bitShift(9) * 0.1, 2);
  var Hz_1s_gust   = precisionRound(Hz_3s_gust + bitShift(8) * 0.1, 2);
  var Hz_3s_min    = precisionRound(bitShift(9) * 0.1, 2);
  var Hz_1s_stdev  = precisionRound(bitShift(8) * 0.1, 2);
  var Dir_avg      = bitShift(9);
  var Dir_gust     = bitShift(9);
  var Dir_stdev    = bitShift(8);
  var Dir_ccw_min  = bitShift(9);
  var Dir_cw_max   = bitShift(9);
  var Time_1s_gust = bitShift(7) * 5;
  var Alarm_sent   = bitShift(1);
  var Dbg          = bitShift(5);

  var Battery_V = precisionRound((Index % 5) * 0.2 + 3.3, 4);

  return {
    "m01_Index": Index,
    "m02_BattState": BattState,
    "m03_Battery_V": Battery_V,
    "m04_Hz_avg": Hz_avg,
    "m05_Hz_3s_gust": Hz_3s_gust,
    "m06_Hz_1s_gust": Hz_1s_gust,
    "m07_Hz_3s_min": Hz_3s_min,
    "m08_Hz_1s_stdev": Hz_1s_stdev,
    "m04b_Wind_avg_ms":     windSpeedMs(Hz_avg),
    "m05b_Wind_3s_gust_ms": windSpeedMs(Hz_3s_gust),
    "m06b_Wind_1s_gust_ms": windSpeedMs(Hz_1s_gust),
    "m07b_Wind_3s_min_ms":  windSpeedMs(Hz_3s_min),
    "m09_Dir_avg": Dir_avg,
    "m10_Dir_gust": Dir_gust,
    "m11_Dir_stdev": Dir_stdev,
    "m12_Dir_ccw_min": Dir_ccw_min,
    "m13_Dir_cw_max": Dir_cw_max,
    "m14_Time_1s_gust_s": Time_1s_gust,
    "m15_Alarm_sent": Alarm_sent,
    "m16_Dbg": Dbg,
    "frm_payload": fwd_frm_payload,
    "payload_hex": fwd_payload_hex,
    "f_port": port,
    "f_cnt": Index
  };
}

function consume(event) {
  var payload = event.data.payloadHex;
  var port = event.data.port;
  var bytes = Hex.hexToBytes(payload);
  var decoded = Decoder(bytes, port);
  emit('sample', { data: decoded, topic: "default" });
}

Appendix B - webhook custom payload (TTN v3)

This is the unwrapped ApplicationUp body. allMETEO's tti/uplink.php reads $data['end_device_ids']['dev_eui'], $data['uplink_message']['frm_payload'], ['f_port'], ['f_cnt'] and ['received_at'] - all at the root. Do not wrap it in the events envelope (name / identifiers / data); that nests these keys one level too deep and the frame is silently dropped. application_id live-2020-10 tags the device with the same source as the TTI fleet.

{
  "end_device_ids": {
    "device_id": "2605lw044",
    "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": "58A0CBFFFE804E07" }, "rssi": -82, "snr": 10.5 }
    ],
    "settings": {
      "data_rate": { "lora": { "spreading_factor": 7, "bandwidth": 125000 } }
    }
  }
}

Appendix C - reference test vectors

Run these through the decoder to confirm it behaves; values are verified.

Payload (hex)                     Index  Hz_avg  Dir_avg   frm_payload (base64)
0a0138401c000c56410a9594eddf      10     0.78    86 deg   CgE4QBwADFZBCpWU7d8=
2b00784400000a61280d1857655f      43     0.30    97 deg   KwB4RAAACmEoDRhXZV8=
1000001c0c00034ba481522969d9      16     0.00    331 deg  EAAAHAwAA0ukgVIpadk=

Appendix D - changing OTAA credentials (Port 100)

Advanced / one-way risky operation

Port 100 rewrites the device's join credentials (AppKey + AppEUI). If the device applies new keys and the join server does not hold the matching values, the OTAA join fails and the unit goes dark - potentially a site visit. Read all steps before sending.

Payload: 24 bytes = [ AppKey : 16 B ][ AppEUI : 8 B ] (48 hex chars), Port 100. To change only the AppEUI, keep the current AppKey and append the new AppEUI:

[ AppKey : 16 bytes ][ AppEUI : 8 bytes ]   ->  24 bytes = 48 hex chars, Port 100

Change ONLY the AppEUI -> keep the current AppKey, append the new AppEUI:
[ current AppKey 16 B ][ new AppEUI 8 B ]

Step by step

  • 1. Prepare. Decide the new AppEUI (and/or AppKey). Confirm you can update the join server for this DevEUI - on managed connectivity (akenza CaaS / Swisscom) that is the provider's join server, not a local setting.
  • 2. Confirm byte order. The 24-byte write must use the order the firmware expects. LoRaWAN *join frames* carry EUIs LSB-first (reversed); config *writes* are often MSB-first (as displayed). Reversed bytes write the wrong keys.
  • 3. Assemble the payload. Concatenate AppKey (16 B) + AppEUI (8 B) into one 48-hex-char string in the confirmed byte order.
  • 4. Queue the downlink. Device -> Downlink tab -> Raw (bypasses the encoder) -> Payload HEX = the 48-char string -> Port = 100 -> enable Confirmed downlink -> Send message.
  • 5. Wait for delivery. Class A: the downlink is delivered only in the RX window after an uplink (~10-min cadence), so it queues. Watch for the confirmed ACK and for the device applying it.
  • 6. Update the join server. Register the new AppKey/AppEUI for this DevEUI on the LNS (akenza/Swisscom) so the next OTAA join succeeds. The join server must hold the new keys before the device's next rejoin.
  • 7. Verify rejoin. Confirm the device rejoins with the new keys and uplinks resume. Any mismatch and it stops joining.
Current values (verify against the serial sheet)

AppKey 424152414E492044455349474E000000 ("BARANI DESIGN"); AppEUI 424152414E490000. Full per-port command details live in the separate command reference.