General Information

The Dzero Gateway 4X is a device that can be used to readout modern utility meters with standardized infrared "INFO" interface.

The meter outputs over its infrared "INFO" interface a serial protocol conforming to the Smart Meter Language Protocol 1.04 (SML) or IEC 62056-21 protocol. This interface is intended to be used by end-users and not for billing purposes of the electricity supplier.

The read information normally contains the current consumption values of the meter and gets interpreted and forwarded by the Dzero Gateway 4X via a LoRaWAN or Nb-IoT to web based applications interested in further processing this data.

Dimensions

Download_Dimensions

Datasheet

Download_Datasheet


Warning

Older meters with "infrared pulse" output are not compatible to the Dzero Gateway 4X. Please check our list of compatible meters to make sure it is equipped with the correct interface.

Hint

Consider using the latest firmware on your hardware

  • See available firmware downloads

Compatible utility meters

Any meter that adheres to the standard can be read. The following list contains meters that we successfully tested.

Electricity meter

Manufacturer

DTZ541-ZEBAHolley
LK13 seriesLogarex
OpenWay® 3.HZiTron
SGM-C4 seriesefr
SGM-D seriesefr
eHZ-K seriesEMH
mMe4.0 seriesEMH
ED300 seriesEMH
eBZD seriesEMH
E320Landis+Gyr
MT681

ISKRA

Quickstart

  1. Connect to the device with the Lobaro Tool using the Lobaro Config Adapter
  2. Under Configuration click "Reload Config" and change the fields "ReadCron" and "ObisCode" for all connected heads as needed as well as choosing the desired Network Connection by setting the "WAN" followed by clicking on "Write to Device". The RJ12 port near the 3.5 mm jack is port 1, the RJ12 port near the Antenna is port 4.
  3. If you selected lorawan as WAN: Register the device in your LoRaWAN network
  4. Place the EDL21 opto head on the "Info" interface
  5. Restart the device by pressing the reset button


Configuration

The configuration is done using Lobaro Maintenance Tool and the Lobaro USB PC adapter or remote via LoRaWAN downlink (see LoRaWAN Downlink Config) or LTE.

Network (general)

Name#LoRaWAN-RemoteConfigurationDescriptionDefault ValueValue Description & Examples
WANRadio technology used for connection to backendlte
  • lte: use either cellular NB-IoT or LTE-M
  • nbiot: use cellular NB-IoT
  • ltem: use cellular LTE-M
  • lorawan: use LoRaWAN with OTAA
  • lorawan-abp: use LoRaWAN with ABP
Host


LoRaWAN

The connection to the LoRaWAN network is defined by multiple configuration parameters. This need to be set according to your LoRaWAN network and the way your device is supposed to be attached to it, or the device will not be able to send any data.

For a detailed introduction into how this values need to be configured, please refer to the chapter LoRaWAN configuration in our LoRaWAN background article.

Name#LoRaWAN-RemoteConfigurationDescriptionDefault ValueValue Description & Examples
LostRebootDays without downlink before reboot (triggers downlinks)5days, 0=don't reboot
DevEUIDevEUI used to identify the Device
e.g. 0123456789abcdef
AppKeyKey used for OTAA (v1.0 and v1.1)

NwkKeyKey used for OTAA (v1.1 only)

JoinEUIUsed for OTAA (called AppEUI in v1.0)
e.g. 0123456789abcdef
SFSpreading Factor12

LTE

Name#LoRaWAN-RemoteConfigurationDescriptionDefault ValueValue Description & Examples
HostHostname / IP of the Lobaro Platform APIcoaps://platform.lobaro.com,coap://platform.lobaro.com
OperatorMobile Operator Code (optional)2620126201 (=Deutsche Telekom), for other operators, see above. Empty = Auto detect (longer connecting time)
BandNB-IoT Band
"8", "20", "8,20", Empty = Auto detect (longer connecting time)
APNMobile operator APN (optional)*

1nce: iot.1nce.net

Vodafone Easy Connect: lpwa.vodafone.com (l = littel L)

PINSIM PIN (optional)
Empty or 4 digits (e.g. 1234)
DNSDNS Server9.9.9.9,1.1.1.1
UseNbiot
true
UseLtem
true

wMBus (experimental)

Name#LoRaWAN-RemoteConfigurationDescriptionDefault ValueValue Description & Examples
EncryptionMode775 or 7

Operation

Configuration values defining the behaviour of the device.

Name#LoRaWAN-RemoteConfigurationDescriptionDefault ValueValue Description & Examples
ObisCode1Comma separated list of ObisCodes to select a subset of the available information on port 11-0:1.8.0*255
ObisCode2Comma separated list of ObisCodes to select a subset of the available information on port 21-0:2.8.0*255
ObisCode3Comma separated list of ObisCodes to select a subset of the available information on port 31-0:1.8.0*255
ObisCode4Comma separated list of ObisCodes to select a subset of the available information on port 41-0:2.8.0*255
PayloadFormatFormat used for data upload (include timestamps or not)int1=no timestamp, 2=include timestamp
ReadCronCron expression defining when to read and upload0 0/15 * * * *0 0/15 * * * * for every 15 minutes
VerboseLoggingEnables additional Debug outputfalsetrue = enabled, false = disabled


See also our Introduction to Cron expressions and our Introduction to Obis Codes.

LED blinking patterns

The following pattery are explained in the order in which they appear after initial power on / reset of the device.

red/green/blue500 ms eachinitial pattern after reset



Payload (LoRaWAN)

Payload

Payload Format 1 (Port 3, with exponent)

This Format is used, when the configuration parameter PayloadFormat is set to 1. Please refer to the documentation of Format 2. The only differences are the port and that there is no Unix Timestamp included in the header.

Payload Format 2 (Port 4, with exponent and timestamp)

This Format is used, when the configuration parameter PayloadFormat is set to 2 .

Each uploaded LoRaWAN-Message with data is prefixed with a 5 byte Timestamp, indicating when the values where received from the attached meter. This allows for a more precise timing information then using the time of reception, as the upload can be delayed quite heavily due to our random delay feature and potentially due to duty cycle restrictions. The timestamp also makes it easy to reassociate values from multiple uplinks to a single reading, when multiple uplinks must be used to upload all values. If a readout is spilt over multiple uplinks (because of LoRaWAN's length restrictions), every uplink from that reading will have the same timestamp (which is the time of received the values from the meter).

The Timestamp is sent as a UNIX-Timestamp encoded as a bigendian signed 40-bit number.

"Unit" is not yet implemented an will always be zero. Please be aware that SML relates to SI units whereas IEC can output units like kWh. The output of the device will not be automatically converted to SI units.

The payload consists of a header and multiple entries, one entry per OBIS code given in the configuration. Each entry follows the following structure:

Header

Head NumberUnix Timestamp
1 byte5 bytes

Entry

OBISCode (hex)

length of value (n)

value

exponent

unit
6 bytes1 byten bytes, LSB first1 byte (signed)1 byte

Example packet: 03 00 63 d1 47 80 01 00 01 08 00 ff 03 14 e3 31 ff 00 01 00 01 08 00 FE 08 FF 02 00 00 00 00 00 00 02 00


Header:


Head Number

Unix Timestamp

Bytes03 00 63 d1 47 80
DescriptionOpto Head connected to Port 3Wednesday, 25. January 2023 15:15:12

Entry 1:


OBISCode (hex)

length of value (n)

value

exponent

unit
Bytes01 00 01 08 00 FF0314 e3 31 (=3269396)ff00
Description1-0:1.8.0*2553326939.6 (3269396*10^-1)-10

Entry 2:


OBISCode (hex)

length of value (n)

value

exponent

unit
Bytes01 00 01 08 00 FE08FF 02 00 00 00 00 00 00 (=767)0200
Description1-0:1.8.0*254876700 (767*10^2)20

Multiple messages

The Bridge puts as many values in a single data message as possible (respecting the current Spreading Factor). When it cannot fit all values in a single message, it will send multiple data messages until all values are uploaded. It will never split a single value. Since every value is prefixed with the Obis code, the parser can easily assign values to Obis codes.



Status Message (Port 64)

Example Payload:  45 44 4c 00 00 01 00 00 00 0e 31 00 dc 00


namelentypedescriptionin example
Firmware Identifier3String3 Charcter FW Identifier45 44 4c → EDL
Firmware Version3

uint8[3]

Version: <major>.<minor>.<patch>00 00 01 → 0.0.1
Status1uint8RFU - always 000
Reboot reason1uint8RFU - always 000
Final words1uint8RFU - always 000
vBat2int16battery Voltage in mV0e 31 → 3633 mV → 3.633 V
Temperature2int16Internal temperature from controller in 1/10 °C00 dc →220 → 22.0 °C
Custom Status1uint8, bitfieldIndicates global errors, i.e. if bit 5 is set no opto head could be found00 → no error, everything is fine

Payload (LTE)

Handled by the Platform

Payload (wMBus)

See Standard. APPKey will be used as key for encryption.


Reference decoder

This is a decoder written in JavaScript that can be used to parse the device's messages.


function readName(bytes, i) {
    return bytes.slice(i, i + 6);
}

function readValue(len, bytes, i) {
    if (len <= 0) {
        return [];
    }
    return bytes.slice(i, i + len);
}

function toHexString(byteArray) {
    var s = '';
    byteArray.forEach(function (byte) {
        s += ('0' + (byte & 0xFF).toString(16)).slice(-2);
    });
    return s;
}

function parse_sint16(bytes, idx) {
    bytes = bytes.slice(idx || 0);
    var t = bytes[0] << 8 | bytes[1] << 0;
    if( (t & 1<<15) > 0){ // temp is negative (16bit 2's complement)
        t = ((~t)& 0xffff)+1; // invert 16bits & add 1 => now positive value
        t=t*-1;
    }
    return t;
}

function parse_uint16(bytes, idx) {
    bytes = bytes.slice(idx || 0);
    var t = bytes[0] << 8 | bytes[1] << 0;
    return t;
}


function signed(val, bits) {
    if ((val & 1 << (bits - 1)) > 0) { // value is negative (16bit 2's complement)
        var mask = Math.pow(2, bits) - 1;
        val = (~val & mask) + 1; // invert all bits & add 1 => now positive value
        val = val * -1;
    }
    return val;
}
function uint40_BE(bytes, idx) {
    bytes = bytes.slice(idx || 0);
    return bytes[0] << 32 |
        bytes[1] << 24 | bytes[2] << 16 | bytes[3] << 8 | bytes[4] << 0;
}
function uint16_BE(bytes, idx) {
    bytes = bytes.slice(idx || 0);
    return bytes[0] << 8 | bytes[1] << 0;
}
function int40_BE(bytes, idx) {return signed(uint40_BE(bytes, idx), 40);}
function int16_BE(bytes, idx) {return signed(uint16_BE(bytes, idx), 16);}
function int8(bytes, idx) {return signed(bytes[idx || 0], 8);}

function toNumber(bytes) {
    var res = 0;

    for (var i = bytes.length-1; i >= 0 ; i--) {
        res *= 256;
        res += bytes[i];
    }
    
    return res;
}

function readVersion(bytes) {
    if (bytes.length<3) {
        return null;
    }
    return "v" + bytes[0] + "." + bytes[1] + "." + bytes[2];
}

function decodeStatus(bytes) {
    var decoded = {
        "version":readVersion(bytes),
        "flags": bytes[3],
        "vBat": uint16_BE(bytes, 4) / 1000,
        "temp": int16_BE(bytes, 6) / 10,
    };

    if (Device) {
        Device.setProperty("version", decoded.version);
        Device.setProperty("voltage", decoded.vBat);
        Device.setProperty("temperature", decoded.temp);
        Device.setProperty("status_flags", decoded.flags);
    }

    return decoded;
}

function decodeSmlValuesV1(bytes) {
    var decoded = {
        values: [],
    };

    if (bytes.length === 1) {
        // No Data! Read error?
        return decoded;
    }

    var pos = 0;
    while (pos < bytes.length) {
        var name = readName(bytes, pos);
        pos += 6;
        var len = bytes[pos];
        pos += 1;
        var value = readValue(len, bytes, pos);
        pos += len;

        var val = {
            nameHex: toHexString(name),
            len: len,
            value: toNumber(value),
            valueHex: toHexString(value)
        };

        decoded.values.push(val);
    }

    return decoded;
}

function decodeSmlValuesV2(bytes) {
    var decoded = {
        values: [],
    };

    if (bytes.length === 1) {
        // No Data! Read error?
        return decoded;
    }

    var pos = 0;
    var headNo = bytes[0];
    decoded.headNo = headNo;
    pos += 1;
    while (pos < bytes.length) {
        var name = readName(bytes, pos);
        pos += 6;
        var len = bytes[pos];
        pos += 1;
        var value = readValue(len, bytes, pos);
        pos += len;
        if (len > 0) {
            var exponent = int8(bytes, pos);
            pos += 1;
        }
        if (len > 0) {
            var unit = int8(bytes, pos);
            pos += 1;
        }
        var val;
        if (len > 0) {
            val = {
                obiscode: name[0] + "-" + name[1] +":" + name[2] + "." + name[3] + "." +name[4] + "*" +name[5],
                //len: len,
                value: toNumber(value) * Math.pow(10, exponent),
                unit: toNumber(unit),
                //valueHex: toHexString(value),
            }
        } else {
            val = {
                obiscode: name[0] + "-" + name[1] +":" + name[2] + "." + name[3] + "." +name[4] + "*" +name[5],
                //len: len,
                value: toNumber(value),
                unit: toNumber(unit),
                //valueHex: toHexString(value),
            }
        }

        decoded.values.push(val);
    }
    return decoded;
}

function decodeSmlValuesV3(bytes) {
      var decoded = {
        values: [],
    };

    if (bytes.length === 1) {
        // No Data! Read error?
        return decoded;
    }

    var pos = 0;
    var headNo = bytes[0];
    decoded.headNo = headNo;
    pos += 1;
    
    decoded.time = int40_BE(bytes, 1) * 1000;
    pos+=5;
    
    while (pos < bytes.length) {
        var name = readName(bytes, pos);
        pos += 6;
        var len = bytes[pos];
        pos += 1;
        var value = readValue(len, bytes, pos);
        pos += len;
        if (len > 0) {
            var exponent = int8(bytes, pos);
            pos += 1;
        }
        if (len > 0) {
            var unit = int8(bytes, pos);
            pos += 1;
        }
        var val;
        if (len > 0) {
            val = {
                obiscode: name[0] + "-" + name[1] +":" + name[2] + "." + name[3] + "." +name[4] + "*" +name[5],
                //nameHex: toHexString(name),
                //len: len,
                value: toNumber(value) * Math.pow(10, exponent),
                //valueHex: toHexString(value),
                unit: toNumber(unit),
            }
        }
        else {
            val = {
                obiscode: name[0] + "-" + name[1] +":" + name[2] + "." + name[3] + "." +name[4] + "*" +name[5],
                //len: len,
                value: toNumber(value),
                //valueHex: toHexString(value),
                unit: toNumber(unit),
            }
        }

        decoded.values.push(val);
    }
    return decoded;
}

function decode_status_code(code) {
  switch (code) {
    case 0:
      return "OK";
    default:
      return "UNKNOWN";
  }
}

function decode_reboot_reason(code) {
  // STM reboot code from our HAL:
  switch (code) {
    case 1:
      return "LOW_POWER_RESET";
    case 2:
      return "WINDOW_WATCHDOG_RESET";
    case 3:
      return "INDEPENDENT_WATCHDOG_RESET";
    case 4:
      return "SOFTWARE_RESET";
    case 5:
      return "POWER_ON_RESET";
    case 6:
      return "EXTERNAL_RESET_PIN_RESET";
    case 7:
      return "OBL_RESET";
    default:
      return "UNKNOWN";
  }
}

function DecoderPort64(bytes) {
  // legacy format, firmware 4.x
  // Decode an uplink message from a buffer
  // (array) of bytes to an object of fields.
  var firmware = String.fromCharCode.apply(null, bytes.slice(0, 3));
  var version = readVersion(bytes, 3);
  var status_code = bytes[6];
  var status_text = decode_status_code(status_code);
  var reboot_code = bytes[7];
  var reboot_reason = decode_reboot_reason(reboot_code);
  var final_code = bytes[8];
  var vcc = (parse_uint16(bytes, 9) / 1000) || 0.0;
  var temp = (parse_sint16(bytes, 11) / 10) || -0x8000;
  var error_state = bytes[13];
  
  Device.setProperty("firmware", firmware);
  Device.setProperty("version", version);
  Device.setProperty("status_code", status_code);
  Device.setProperty("status_text", status_text);
  Device.setProperty("reboot_code", reboot_code);
  Device.setProperty("reboot_reason", reboot_reason);
  Device.setProperty("final_code", final_code);
  Device.setProperty("error_state", error_state);
  Device.setProperty("temperature", temp);
  Device.setProperty("voltage", vcc);
  
  return {
    "firmware": firmware,
    "version": version,
    "status_code": status_code,
    "status_text": status_text,
    "reboot_code": reboot_code,
    "reboot_reason": reboot_reason,
    "final_code": final_code,
    "temperature": temp,
    "voltage": vcc,
    "error_state": error_state
  };
}

function Decoder(bytes, port) {
    // Decode an uplink message from a buffer
    // (array) of bytes to an object of fields.
    switch (port) {
        case 1:
            return decodeStatus(bytes);
        case 2:
            return decodeSmlValuesV1(bytes);
        case 3:
            return decodeSmlValuesV2(bytes);
        case 4:
            return decodeSmlValuesV3(bytes);
        case 64: 
            return DecoderPort64(bytes);
    }
}

function NB_ParseDeviceQuery(input) {
    for (var key in input.d) {
        var v = input.d[key];
        switch (key) {
            case "temperature":
                v = v / 10.0;
                break;
            case "vbat":
                v = v / 1000.0;
                //NB_SetBatteryStatus(v)
                break;
        }
        Device.setProperty("device." + key, v);
    }
    return null;
}

function NB_ParseConfigQuery(input) {
    for (var key in input.d) {
        Device.setConfig(key, input.d[key]);
    }
    return null;
}

function ParseV2(input) {
  var decoded = {
        values: [],
    };
  
  decoded.headNo = input.d.head;
  decoded.time = new Date(input.d.timestamp*1000).toISOString();
  
  
  //return toNumber(parseBase64(input.d.batch[0].value)) * Math.pow(10, input.d.batch[0].scaler);
  //decoded.payload = input.d;
  var val;
  
  if ((input.d.batch) && (input.d.batch.length > 0) ) {
    for( i = 0; i<input.d.batch.length; i++){
      val = {
          obiscode: input.d.batch[i].obiscode,
          value: input.d.batch[i].value * Math.pow(10, input.d.batch[i].scaler),
          //valueHex: toHexString(input.d.batch[i].value),
          unit: toNumber(input.d.batch[i].unit),
      }
      decoded.values.push(val);
    }
  }
  return decoded;
}

function NB_ParseStatusQuery(input) {
  for (var key in input.d) {
    var v = input.d[key];
      switch (key) {
          case "temperature":
              v = v / 10.0;
              break;
          case "vbat":
              v = v / 1000.0;
              //NB_SetBatteryStatus(v)
              continue;
      }
      Device.setProperty("device." + key, v);
  }
  return null;
}


function ParseNBiot(input) {
  var query = input.q || "data";
  var res = null;
  switch (query) {
        case "device":
            res = NB_ParseDeviceQuery(input); 
            break;
        case "config":
            res = NB_ParseConfigQuery(input); 
            break;
        case "data":
            res = ParseV2(input); 
            break;
        case "status":
            res = NB_ParseStatusQuery(input); 
            break;
              
        default:
            throw new Error("Unknown message type: '" + query + "'");
  }
  
  if (res != null) {
    res.addr = input.i; 
    res.fCnt = input.n; 
  }
  return res;
}

// Wrapper for Lobaro Platform
function Parse(input) {
    // Decode an incoming message to an object of fields.
    var b = bytes(atob(input.data));
    
    // NB-IoT
    if (input.d) {
      return ParseNBiot(input)
    }
    
    // LoRaWAN
    return Decoder(b, input.fPort);
}