Skip to content

Commit

Permalink
Add 2 new Elvaco devices (#657)
Browse files Browse the repository at this point in the history
* Add 2 new Elvaco devices

* refactor and add decoder for cmi4130

* add cmi4130.yaml

* add image for cmi4111 and cmi4130

* fixed files

* fixed files

---------

Co-authored-by: Jaime Trinidad <[email protected]>
Co-authored-by: Jaime Trinidad <[email protected]>
  • Loading branch information
3 people authored Feb 28, 2024
1 parent 1988801 commit 9e636ca
Show file tree
Hide file tree
Showing 19 changed files with 928 additions and 0 deletions.
19 changes: 19 additions & 0 deletions vendor/elvaco/cmi4110-codec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Uplink decoder decodes binary data uplink into a JSON object (optional)
# For documentation on writing encoders and decoders, see: https://www.thethingsindustries.com/docs/integrations/payload-formatters/javascript/
uplinkDecoder:
fileName: cmi4110.js
examples:
- description: Landis UH temperature, energy, volume, power, flow, flow_temperature, return_temperature, serial, and error flag.
input:
fPort: 2
bytes: [0, 12, 6, 82, 103, 97, 2, 12, 20, 151, 137, 153, 0, 11, 45, 0, 0, 0, 11, 59, 0, 0, 0, 10, 90, 51, 6, 10, 94, 65, 5, 12, 120, 41, 17, 3, 102, 2, 253, 23, 0, 0]
output:
data:
energy: 2616752
volume: 9989.97
power: 0
flow: 0
flow_temperature: 63.3
return_temperature: 54.1
serial: 66031129
error_flag: 0
49 changes: 49 additions & 0 deletions vendor/elvaco/cmi4110-profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
vendorProfileID: 574

# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1
macVersion: '1.0.2'
# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version:
# 1.0: TS001-1.0
# 1.0.1: TS001-1.0.1
# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB
# 1.0.3: RP001-1.0.3-RevA
# 1.0.4: RP002-1.0.0 or RP002-1.0.1
# 1.1: RP001-1.1-RevA or RP001-1.1-RevB
regionalParametersVersion: 'RP001-1.0.2'

# Whether the end device supports join (OTAA) or not (ABP)
supportsJoin: true
# If your device is an ABP device (supportsJoin is false), uncomment the following fields:
# RX1 delay
#rx1Delay: 5
# RX1 data rate offset
#rx1DataRateOffset: 0
# RX2 data rate index
#rx2DataRateIndex: 0
# RX2 frequency (MHz)
#rx2Frequency: 869.525
# Factory preset frequencies (MHz)
#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9]

# Maximum EIRP
maxEIRP: 16
# Whether the end device supports 32-bit frame counters
supports32bitFCnt: true

# Whether the end device supports class B
supportsClassB: false
# If your device supports class B, uncomment the following fields:
# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds)
#classBTimeout: 60
# Ping slot period (seconds)
#pingSlotPeriod: 128
# Ping slot data rate index
#pingSlotDataRateIndex: 0
# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping.
#pingSlotFrequency: 869.525

# Whether the end device supports class C
supportsClassC: false
# If your device supports class C, uncomment the following fields:
# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds)
#classCTimeout: 60
Binary file added vendor/elvaco/cmi4110.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 158 additions & 0 deletions vendor/elvaco/cmi4110.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
const difVifMapping = {
'0c': {
'06': { measure: 'energy', unit: 'kWh', decimal: 0 },
'07': { measure: 'energy', unit: 'kWh', decimal: -1 },
// "FB00": {"measure": "energy", "unit": "kWh", "decimal": -3},
// "FB01": {"measure": "energy", "unit": "kWh", "decimal": -3},
14: { measure: 'volume', unit: 'm3', decimal: 2 },
15: { measure: 'volume', unit: 'm3', decimal: 1 },
16: { measure: 'volume', unit: 'm3', decimal: 0 },
78: { measure: 'serial', unit: '', decimal: 0 },
},
'0b': {
'2a': { measure: 'power', unit: 'kW', decimal: 4 },
'2b': { measure: 'power', unit: 'kW', decimal: 3 },
'2c': { measure: 'power', unit: 'kW', decimal: 2 },
'2d': { measure: 'power', unit: 'kW', decimal: 1 },
'2e': { measure: 'power', unit: 'kW', decimal: 0 },
'2f': { measure: 'power', unit: 'kW', decimal: -1 },
'3b': { measure: 'flow', unit: 'm3/h', decimal: 3 },
'3c': { measure: 'flow', unit: 'm3/h', decimal: 2 },
'3d': { measure: 'flow', unit: 'm3/h', decimal: 1 },
'3e': { measure: 'flow', unit: 'm3/h', decimal: 0 },
'3f': { measure: 'flow', unit: 'm3/h', decimal: -1 },
},
'0a': {
'5a': { measure: 'flow_temperature', unit: '°C', decimal: 1 },
'5b': { measure: 'flow_temperature', unit: '°C', decimal: 0 },
'5e': { measure: 'return_temperature', unit: '°C', decimal: 1 },
'5f': { measure: 'return_temperature', unit: '°C', decimal: 0 },
},
'02': { fd17: { measure: 'error_flag', unit: '', decimal: 0 } },
'04': { fd17: { measure: 'error_flag', unit: '', decimal: 0 } },
};

function getVif(payloadArr, index) {
const dif = payloadArr[index].toLowerCase();
const dif_int = parseInt(dif, 16);

if (dif_int === 132) {
const vif = payloadArr
.slice(index + 1, index + 3)
.join('')
.toLowerCase();
return [vif, index + 3];
} else {
const vif = payloadArr[index + 1].toLowerCase();
return [vif, index + 2];
}
}

function checkNegativeValue(reversed_values) {
if (reversed_values.includes('f0')) {
return -parseInt(reversed_values.replace('f0', ''), 16);
} else {
return parseInt(reversed_values, 16);
}
}

function decodeCMI4110Standard(payloadArr) {
const decoded_dictionary = {};
let i = 1;
energy_count = 0;
while (i < payloadArr.length) {
const dif = payloadArr[i].toLowerCase();
const dif_int = parseInt(dif, 16);
let [vif, newIndex] = getVif(payloadArr, i);
i = newIndex;
let bcd_len = dif_int;
if (!(dif_int >= 2 && dif_int <= 4)) {
bcd_len = dif_int - 8;
}
if (payloadArr.length - i <= 3) {
vif += payloadArr[i];
i += 1;
}

if (dif in difVifMapping && vif in difVifMapping[dif]) {
const reversed_values = payloadArr
.slice(i, i + bcd_len)
.reverse()
.join('');
const value_int = checkNegativeValue(reversed_values);
i += bcd_len;

const unit_info = difVifMapping[dif][vif];
let register = unit_info['measure'];
switch (register) {
case 'energy':
if (energy_count == 0) {
} else if (energy_count < 4) {
register = 'energy_tariff_' + energy_count.toString();
} else {
throw 'more than 4 energy registers';
}
energy_count += 1;
value = parseInt(reversed_values) / Math.pow(10, unit_info['decimal']);
break;
case 'flow' || 'power':
value = value_int / Math.pow(10, unit_info['decimal']);
break;
default:
value = parseInt(reversed_values) / Math.pow(10, unit_info['decimal']);
if (!unit_info['unit']) {
value = parseInt(reversed_values);
}
}
decoded_dictionary[register] = value;
} else {
throw 'Unknown dif ' + dif + ' and vif ' + vif;
}
}
return decoded_dictionary;
}

function bytesToHexArray(bytes) {
return bytes.map(function (byte) {
return ('0' + (byte & 0xff).toString(16)).slice(-2);
});
}

function hexToBytes(hex) {
return hex.map(function (byte) {
return parseInt(byte, 16);
});
}

function decodeUplink(input) {
switch (input.fPort) {
case 2:
const hex_array = bytesToHexArray(input.bytes);
if (hex_array.length < 40) {
return {
data: {},
errors: ['payload length < 40 '],
};
}
if (hex_array[0] != '00') {
return {
data: {},
errors: ['Payload type unknown, currently standard format supported'],
};
}
return {
data: decodeCMI4110Standard(hex_array),
};
default:
return {
data: {},
errors: ['unknown FPort'],
};
}
}

// array= 000c06526761020c14978999000b2d0000000b3b0000000a5a33060a5e41050c782911036602fd170000
// hex = ["00","0c","06","52","67","61","02","0c","14","97","89","99","00","0b","2d","00","00","00","0b","3b","00","00","00","0a","5a","33","06","0a","5e","41","05","0c","78","29","11","03","66","02","fd","17","00","00"]
bytes = [0, 12, 6, 82, 103, 97, 2, 12, 20, 151, 137, 153, 0, 11, 45, 0, 0, 0, 11, 59, 0, 0, 0, 10, 90, 51, 6, 10, 94, 65, 5, 12, 120, 41, 17, 3, 102, 2, 253, 23, 0, 0];
input = { fPort: 2, bytes: bytes };
// console.log(decodeUplink(input));
35 changes: 35 additions & 0 deletions vendor/elvaco/cmi4110.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CMi4110
description: Heat Meter Connectivity Module (MCM) for Landis+Gyr UH50/UC50
# Hardware versions (optional)
hardwareVersions:
- version: '1.0'
numeric: 1
# Firmware versions (at least one is mandatory)
firmwareVersions:
- # Firmware version
version: '1.0'
numeric: 1
# Supported hardware versions (optional)
hardwareVersions:
- '1.0' # Must refer to hardwareVersions declared above
# LoRaWAN Device Profiles per region
# Supported regions: EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, RU864-870
profiles:
EU863-870:
# Optional identifier of the vendor of the profile. When you specify the vendorID, the profile is loaded from
# the vendorID's folder. This allows you to reuse profiles from module or LoRaWAN end device stack vendors.
# When vendorID is empty, the profile is loaded from the current directory.
vendorID: elvaco
# Unique identifier of the profile (lowercase, alphanumeric with dashes, max 36 characters).
# This is the file name of the profile and must have the .yaml extension.
id: cmi4110-profile
# Specify whether the device is LoRa Alliance certified.
lorawanCertified: true
codec: cmi4110-codec
# US902-928:
# # This is the file name of the profile and must have the .yaml extension.
# id: custom-profile-us915
# # Specify whether the device is LoRa Alliance certified.
# lorawanCertified: true
# # This is the file name of the codec defintion and must have the .yaml extension.
# codec: device-a-codec
Binary file added vendor/elvaco/cmi4111.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 9e636ca

Please sign in to comment.