How to decode Elvaco CMi4110 standard M-Bus payload?

Yes i will post my endversion soon with explanation for other people!

I totally agree that it is not that great but we have to deal with. Maybe for future projects we should look for other sensors which can be more modified or do handel the payload in an other way. Nevertheless i really want to thank you for your patient and help!!! We got the thinks working! Get the payload via node red into an InfluxDB and visualisation with grafana :slight_smile: Thank your very much!!!

For Node-RED you might be able to find some libraries to decode M-Bus for you, rather than using a TTN Payload Format to do the preprocessing. On the other hand, you obviously only need the decoding part, not any communication, so any full M-Bus library might be overkill. Also, just in case the device’s format has some errors, you might not be able to fix those. Still then, the JavaScript in NodeJS is a bit more advanced too, and it would keep all your code in one place.

As for the Energy with 0x0CFB00 and 0x0CFB01, I think that indeed adds another byte to the payload to keep 8 digits for the value, as otherwise its existence makes no sense: 6 digits with one or zero decimals would also fit in the other 0x0C06, 0C07, 0C0E and 0C0F formats. So, the “extended VIF” 0xFB seems to be needed for large values. Which also makes me wonder:

So, the unit is not set for some specific output using some configuration? I think it might not be safe to assume the unit and number of decimals never change. If, over time, the values get to be too large to fit in the fixed number of digits, are you sure the device will not decrease its number of decimals, or select a larger unit, to prevent truncation of its readings?

In case you didn’t do that yet, one can easily create a helper for the packed BCD:

/**
 * Convert the array of bytes into an unsigned integer, assuming packed
 * binary-coded decimal (BCD) with an even number of nibbles, LSB.
 */
function bcdToUint(bytes) {
  return bytes.reduceRight(function(acc, byte) {
    return 100*acc + 10*(byte >> 4) + (byte & 0x0F);
  }, 0);
}

The above can be used with, e.g, Volumen: bcdToUint(bytes.slice(9, 12)) / 100 (where index 12 is not included in the slice of the array, so passes an array of length 3, not 4).

Next, such code is less prone to programming errors when used along with a running index: if some var i has value 9, then it can be incremented by 3 on the fly using addition assignment, when used in bcdToUint(bytes.slice(i, i += 3)) to indicate one wants to consume the next 3 bytes.

And though the documentation seems to suggest a specific order of the data, each part is also uniquely identified by its “VIF”, and its “DIF” specifies the BCD length. That allows for such something quite generic, also supporting the other formats, like:

/**
 * Decoder for M-Bus style payload of the Elvaco CMi4110 sensor.
 * 
 * 2019-11-21: initial, PARTIAL, UNTESTED implementation
 * 
 * Notes:
 *
 * - In M-Bus the length of BCD values can be determined from the data,
 *   but the value multiplier (the number of decimals) is part of the
 *   specification (along with the unit), and not in the data itself.
 *
 * - The documentation seems to suggest the order of DIBs is fixed, but
 *   this decoder does not rely on any order, using the VIF values.
 */

/**
 * Convert the array of bytes into an unsigned integer, assuming packed
 * binary-coded decimal (BCD) with an even number of nibbles, LSB.
 */
function bcdToUint(bytes) {
  return bytes.reduceRight(function(acc, byte) {
    return 100*acc + 10*(byte >> 4) + (byte & 0x0F);
  }, 0);
}

/**
 * Get a hexadecimal representation with leading zeroes for each byte.
 */
function hex(bytes, separator) {
  return bytes.map(function (b) {
    return ('0' + b.toString(16)).substr(-2);
  }).join(separator || '');
}

function Decoder(bytes, port) {
  var result = {};
  var i = 0;

  result.messageType = [
    'standard',
    'compact',
    // 0x02: JSON; not supported in this decoder 
    undefined,
    'scheduled daily redundant',
    'scheduled extended'
  ][bytes[i++]];

  if (!result.messageType) {
    return {
      error: 'unsupported message type',
      unparsed: hex(bytes)
    };
  }

  // Iterate the M-Bus Data Information Blocks, based on their Data
  // Information Field and Value Information Field
  while (i < bytes.length) {
    var dif = bytes[i++];
    var vif = bytes[i++];
  
    var difData = dif & 0x0F;
    var isAccumulated = (dif & 0x40) > 0;
  
    // For this device, actually only BCD4, BCD6 and BCD8 are expected
    var isBcd = (difData >= 0x09 && difData <= 0x0C) || difData === 0x0E;
    var bcdLen = isBcd ? difData & 0x07 : undefined;

    // VIF 0xFB is a special case for large values, handled below. For
    // other BCD values extract the unsigned integer and increase the
    // index here:
    var bcdValue = isBcd && vif !== 0xFB 
      ? bcdToUint(bytes.slice(i, i += bcdLen))
      : undefined;
  
    // We could switch on just VIF, but then unsupported DIF values
    // will go unnoticed
    var difVif = dif << 8 | vif;
    switch (difVif) {

      // Energy, or accumulated energy at 24:00
      case 0x0C06:
      case 0x0C07:
      case 0x4C06:
      case 0x4C07:
        result[isAccumulated ? 'accumulatedEnergy' : 'energy'] = {
          // 0x06 = 3 decimals, 07 = 2 decimals
          value: bcdValue / Math.pow(10, 0x09 - vif),
          unit: 'MWh'
        };
        break;

      case 0x0C0E:
      case 0x0C0F:
      case 0x4C0E:
      case 0x4C0F:
        result[isAccumulated ? 'accumulatedEnergy' : 'energy'] = {
          // 0x0E = 3 decimals, 0F = 2 decimals
          value: bcdValue / Math.pow(10, 0x11 - vif),
          unit: 'GJ'
        };
        break;

      case 0x0CFB:
      case 0x4CFB:
        // Extended VIF, providing an additional byte after VIF
        var vifExtension = bytes[i++];
        bcdValue = bcdToUint(bytes.slice(i, i += bcdLen));
        switch (vifExtension) {
          case 0x00:
          case 0x01:
            result[isAccumulated ? 'accumulatedEnergy' : 'energy'] = {
              // 0x00 = 1 decimal, 01 = no decimals
              value: bcdValue / Math.pow(10, 0x01 - vifExtension),
              unit: 'MWh'
            };
            break;

          case 0x08:
          case 0x09:
            result[isAccumulated ? 'accumulatedEnergy' : 'energy'] = {
              // 0x08 = 1 decimal, 09 = no decimals
              value: bcdValue / Math.pow(10, 0x09 - vifExtension),
              unit: 'GJ'
            };
            break;

          default:
            return {
              error: 'unexpected extended VIF',
              parsed: result,
              unparsed: hex(bytes.slice(i - 3))
            };
        }
        break;

      // Volume
      case 0x0C14:
      case 0x0C15:
      case 0x0C16:
        result.volume = {
          // 0x14 = 2 decimals, 15 = 1, 16 = no decimals
          value: bcdValue / Math.pow(10, 0x16 - vif),
          unit: 'm3'
        };
        break;

      // Power
      case 0x0B2B:
      case 0x0B2C:
      case 0x0B2D:
      case 0x0B2E:
        result.power = {
          // 0x2B = 3 decimals, 2C = 2, 2D = 1, 2E = no decimals
          value: bcdValue / Math.pow(10, 0x2E - vif),
          unit: 'kW'
        };
        break;

      // Flow
      case 0x0B3B:
      case 0x0B3C:
      case 0x0B3D:
      case 0x0B3E:
        result.flow = {
          // 0x3B = 3 decimals, 3C = 2, 3D = 1, 3E = no decimals
          value: bcdValue / Math.pow(10, 0x3E - vif),
          unit: 'm3/h'
        };
        break;

      // Forward temperature
      case 0x0A5A:
      case 0x0A5B:
        result.forwardTemperature = {
          // 0x5A = 1 decimal, 5B = no decimals
          value: bcdValue / Math.pow(10, 0x5B - vif),
          unit: 'C'
        };
        break;

      // Return temperature
      case 0x0A5E:
      case 0x0A5F:
        result.returnTemperature = {
          // 0x5E = 1 decimal, 5F = no decimals
          value: bcdValue / Math.pow(10, 0x5F - vif),
          unit: 'C'
        };
        break;

      // Meter ID (using BCD values)
      case 0x0C78:
        // This will remove leading zeroes in the JSON output
        result.meterId = bcdValue;
        break;

      // Meter date and time
      case 0x046D:
        // Not really implemented yet
        result.dateTime = hex(bytes.slice(i, i += 4));
        break;

      // Error and warning flags
      case 0x02FD:
        // Skip the 0x17 in 02FD17 (we should probably validate it)
        i++;
        result.errorFlags = hex(bytes.slice(i, i += 2));
        break;

      // Error message
      case 0x0E00:
        result.error = 'CMi4110 unable to communicate with UH50/UC50';
        break;

      default:
        return {
          error: 'unexpected format',
          parsed: result,
          unparsed: hex(bytes.slice(i - 2))
        };
    }
  }
  
  return result;
}

For the payload from your first post, this yields (sorted for readability):

{
  "messageType": "standard",
  "energy": {
    "unit": "MWh",
    "value": 5.857
  },
  "volume": {
    "unit": "m3",
    "value": 239.22
  },
  "power": {
    "unit": "kW",
    "value": 15.7
  },
  "flow": {
    "unit": "m3/h",
    "value": 0.82
  },
  "forwardTemperature": {
    "unit": "C",
    "value": 60.6
  },
  "returnTemperature": {
    "unit": "C",
    "value": 44.1
  },
  "meterId": 70183899,
  "errorFlags": "0000"
}

Of course: untested… So, future readers: if you ever use this, compare it to the documentation, and please report back here with some real payload values? Thanks.

2 Likes

I got a reply from Elvaco:

the module will use the same VIF code (unit) as is presented to it by the meter.

I don’t know if that means that the meter always uses the same VIF, or could change the VIF over time when totals get to be large values. But I guess whoever uses these meters knows.

It works. I do get

    {
  "energy": {
    "unit": "MWh",
    "value": 5.857
  },
  "errorFlags": "0000",
  "flow": {
    "unit": "m3/h",
    "value": 0.82
  },
  "forwardTemperature": {
    "unit": "C",
    "value": 60.6
  },
  "messageType": "standard",
  "meterId": 70183899,
  "power": {
    "unit": "kW",
    "value": 15.7
  },
  "returnTemperature": {
    "unit": "C",
    "value": 44.1
  },
  "volume": {
    "unit": "m3",
    "value": 239.22
  }
}

For my Payload

00 0C 06 57 58 00 00 0C 14 22 39 02 00 0B 2D 57 01 00 0B 3B 20 08 00 0A 5A 06 06 0A 5E 41 04 0C 78 99 38 18 70 02 FD 17 00 00

1 Like

Does the MWh for the energy match whatever is on the meter? (This is the 0x0C06 mentioned twice in the manual, with different units…)

  "energy": {
    "unit": "MWh",
    "value": 5.857
  }

Yes the meter does show MWh on its display and also in the payload

1 Like

does anyone have an idea if it is possible to use crypto function in a payload decoder?
I have a mbus message that is encoded with aes key … can aes key be decoded in ttn payload decoder?

There is no mechanism to require or import external libraries. But you can include any JavaScript code by using copy-paste.

However, you’ll need to administer the secrets, and you might not want to share those with TTN, and those might be different for each device in an application in TTN Console. So, it might be easier to do the decryption (and maybe all of the payload decoding) in your own application instead.

thanks for your reply…
the same is my aes key for all devices and security is not important to me in this application…

I would like it to be all in the payload decoder for my data to be written directly into influx integration…

it is somehow most elegant and safest solution for me

Hi folks,

i got a big problem with the decoder.

the meter sends following payload

00 0c0600000000 0c1475430300 0b2d330700 0b3b3019f0 0a5a5002 0a5e8205 0c78 28576468 02fd170000
S	MWh		m³	power	flow	VL	RL	 ID	Meter	no Error

in flow the data is 0b3b3019f0 this result in a to big value. 0f 19 30
in normal operation the meter sens 0b3b804200 leading 2 zeros 00 42 80
it could be an error code for negativ flow.

How to handle this in the codec?

This also happens foe power readings.

regards Maik