From 39a063046db1600ae3d1174dec6610c19598cd3c Mon Sep 17 00:00:00 2001 From: Dominic Hakke Date: Fri, 20 Mar 2026 21:34:24 +0100 Subject: [PATCH 1/5] Add YOBIIQ EM2101 --- vendors/yobiiq/codecs/em2101.js | 984 ++++++++++++++++++ vendors/yobiiq/codecs/test_decode_em2101.json | 171 +++ vendors/yobiiq/codecs/test_encode_em2101.json | 127 +++ vendors/yobiiq/devices/yobiiq-em2101.toml | 35 + vendors/yobiiq/profiles/EU868-classA.toml | 93 ++ vendors/yobiiq/profiles/EU868-classC.toml | 93 ++ vendors/yobiiq/vendor.toml | 24 + 7 files changed, 1527 insertions(+) create mode 100644 vendors/yobiiq/codecs/em2101.js create mode 100644 vendors/yobiiq/codecs/test_decode_em2101.json create mode 100644 vendors/yobiiq/codecs/test_encode_em2101.json create mode 100644 vendors/yobiiq/devices/yobiiq-em2101.toml create mode 100644 vendors/yobiiq/profiles/EU868-classA.toml create mode 100644 vendors/yobiiq/profiles/EU868-classC.toml create mode 100644 vendors/yobiiq/vendor.toml diff --git a/vendors/yobiiq/codecs/em2101.js b/vendors/yobiiq/codecs/em2101.js new file mode 100644 index 0000000..a500a75 --- /dev/null +++ b/vendors/yobiiq/codecs/em2101.js @@ -0,0 +1,984 @@ +/** + * Codec for EM2101 device : compatible with TTN, ChirpStack v4 and v3, etc... + * Release Date : 16 June 2023 + * Update Date : 18 November 2024 + */ + +// Version Control +var VERSION_CONTROL = { + CODEC : {VERSION: "1.0.1", NAME: "codecVersion"}, + DEVICE: {MODEL : "EM2101", NAME: "genericModel"}, + PRODUCT: {CODE : "P1002009", NAME: "productCode"}, + MANUFACTURER: {COMPANY : "YOBIIQ B.V.", NAME: "manufacturer"}, +} + +// Configuration constants for device basic info and current settings +var CONFIG_INFO = { + FPORT : 50, + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "0x09" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false}, + "0x0A" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false}, + "0x16" : {SIZE : 4, NAME : "deviceSerialNumber"}, + "0x0F" : {SIZE : 1, NAME : "deviceClass", + VALUES : { + "0x00" : "Class A", + "0x01" : "Class B", + "0x02" : "Class C", + }, + }, + "0x0B" : {SIZE : 1, NAME : "powerEvent", + VALUES : { + "0x00" : "AC Power Off", + "0x01" : "AC Power On", + }, + }, + "0x00" : {SIZE : 1, NAME : "relayStatus", + VALUES : { + "0x00" : "LOW", + "0x01" : "HIGH" + }, + }, + "0x1E" : {SIZE : 2, NAME : "primaryCurrentTransformerRatio",}, + "0x1F" : {SIZE : 1, NAME : "secondaryCurrentTransformerRatio",}, + "0x20" : {SIZE : 4, NAME : "primaryVoltageTransformerRatio",}, + "0x21" : {SIZE : 2, NAME : "secondaryVoltageTransformerRatio",}, + "0x28" : {SIZE : 0, NAME : "deviceModel",}, + "0x3C" : {SIZE : 2, NAME : "currentLimitFallback", UNIT : "A", RESOLUTION : 0.1,}, + "0x3D" : {SIZE : 2, NAME : "voltageLimitFallback", UNIT : "V",}, + "0x3E" : {SIZE : 2, NAME : "powerLimitFallback", UNIT : "W",}, + "0x3F" : {SIZE : 2, NAME : "deactivationDelayFallback", UNIT : "s",}, + "0x40" : {SIZE : 2, NAME : "activationDelayFallback", UNIT : "s",}, + "0x41" : {SIZE : 2, NAME : "offsetCurrentFallback", UNIT : "A", RESOLUTION : 0.1,}, + "0x42" : {SIZE : 2, NAME : "offsetDelayFallback", UNIT : "s",}, + "0x43" : {SIZE : 2, NAME : "resetTimeFallback", UNIT : "s",}, + "0x44" : {SIZE : 1, NAME : "resetAmountFallback",}, + "0x50" : {SIZE : 2, NAME : "currentLimitDynamic", UNIT : "A", RESOLUTION : 0.1}, + "0x51" : {SIZE : 2, NAME : "voltageLimitDynamic", UNIT : "V",}, + "0x52" : {SIZE : 2, NAME : "powerLimitDynamic", UNIT : "W",}, + "0x53" : {SIZE : 2, NAME : "deactivationDelayDynamic", UNIT : "s",}, + "0x54" : {SIZE : 2, NAME : "activationDelayDynamic", UNIT : "s",}, + "0x55" : {SIZE : 2, NAME : "offsetCurrentDynamic", UNIT : "A", RESOLUTION : 0.1}, + "0x56" : {SIZE : 2, NAME : "offsetDelayDynamic", UNIT : "s",}, + "0x57" : {SIZE : 2, NAME : "resetTimeDynamic", UNIT : "s",}, + "0x58" : {SIZE : 1, NAME : "resetAmountDynamic",}, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +// Configuration constants for measurement registers + var CONFIG_MEASUREMENT = { + FPORT_MIN : 1, + FPORT_MAX : 10, + TYPES : { + "0x00" : {SIZE : 4, NAME : "index",}, + "0x01" : {SIZE : 4, NAME : "timestamp",}, + "0x03" : {SIZE : 4, NAME : "dataloggerTimestamp",}, + "0x04" : {SIZE : 4, NAME : "activeEnergyImportL1T1", UNIT : "Wh",}, + "0x05" : {SIZE : 4, NAME : "activeEnergyImportL1T2", UNIT : "Wh",}, + "0x06" : {SIZE : 4, NAME : "activeEnergyExportL1T1", UNIT : "Wh",}, + "0x07" : {SIZE : 4, NAME : "activeEnergyExportL1T2", UNIT : "Wh",}, + "0x08" : {SIZE : 4, NAME : "reactiveEnergyImportL1T1", UNIT : "varh",}, + "0x09" : {SIZE : 4, NAME : "reactiveEnergyImportL1T2", UNIT : "varh",}, + "0x0A" : {SIZE : 4, NAME : "reactiveEnergyExportL1T1", UNIT : "varh",}, + "0x0B" : {SIZE : 4, NAME : "reactiveEnergyExportL1T2", UNIT : "varh",}, + "0x0C" : {SIZE : 4, NAME : "voltageL1N", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x10" : {SIZE : 4, NAME : "currentL1", UNIT : "mA", SIGNED : true,}, + "0x14" : {SIZE : 4, NAME : "activePowerL1", UNIT : "W", SIGNED : true,}, + "0x17" : {SIZE : 4, NAME : "reactivePowerL1", UNIT : "kvar", RESOLUTION : 0.001, SIGNED : true,}, + "0x1A" : {SIZE : 4, NAME : "apparentPowerL1", UNIT : "kVA", RESOLUTION : 0.001, SIGNED : true,}, + "0x1D" : {SIZE : 1, NAME : "powerFactorL1", RESOLUTION : 0.01, SIGNED : true,}, + "0x20" : {SIZE : 2, NAME : "phaseAngleL1", UNIT : "degree", RESOLUTION : 0.01, SIGNED : true,}, + "0x23" : {SIZE : 2, NAME : "frequency", UNIT : "Hz", RESOLUTION : 0.01, SIGNED : true,}, + "0x24" : {SIZE : 4, NAME : "totalSystemActivePower", UNIT : "kW",}, + "0x25" : {SIZE : 4, NAME : "totalSystemReactivePower", UNIT : "kvar", RESOLUTION : 0.001,}, + "0x26" : {SIZE : 4, NAME : "totalSystemApparentPower", UNIT : "kVA", RESOLUTION : 0.001,}, + "0x27" : {SIZE : 4, NAME : "maximumL1CurrentDemand", UNIT : "mA", SIGNED : true,}, + "0x2A" : {SIZE : 4, NAME : "averagePower", UNIT : "W", SIGNED : true,}, + "0x2B" : {SIZE : 4, NAME : "midYearOfCertification",}, + "0xF0" : {SIZE : 2, NAME : "manufacturedYear", DIGIT: true,}, + "0xF1" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false,}, + "0xF2" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false,}, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +// Configuration constants for change of state +var CONFIG_STATE = { + FPORT : 11, + TYPES : { + "0x01" : {SIZE : 1, NAME : "relayStatus", + VALUES : { + "0x00" : "OPEN", + "0x01" : "CLOSED" + }, + }, + "0x02" : {SIZE : 1, NAME : "digitalInputStatus", + VALUES : { + "0x00" : "OPEN", + "0x01" : "CLOSED" + }, + }, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +// Configuration constants for event logging +var CONFIG_LOGGING = { + FPORT : 60, + CHANNEL : parseInt("0xFD", 16), + TYPES : { + "0x01" : {SIZE : 1, NAME : "relaySwitchingOffReason", + VALUES : { + "0x00" : "Invalid", + "0x01" : "Due to too high current limit", + "0x02" : "By control from the Lora network", + "0x03" : "By operation via display" + }, + }, + "0x02" : {SIZE : 1, NAME : "relayEnableReason", + VALUES : { + "0x00" : "Invalid", + "0x01" : "By reset based on time", + "0x02" : "By reset from the Lora network", + "0x03" : "By operation via display", + "0x04" : "By control from the Lora network" + }, + }, + "0x03" : {SIZE : 4, NAME : "relaySwitchOffTime",}, + "0x04" : {SIZE : 4, NAME : "relayEnableTime",}, + "0x05" : {SIZE : 4, NAME : "currentWhenRelaySwitchingOff",}, + "0x06" : {SIZE : 4, NAME : "voltageWhenRelaySwitchingOff",}, + "0x07" : {SIZE : 4, NAME : "activePowerWhenRelaySwitchingOff",}, + "0x08" : {SIZE : 1, NAME : "resetAmountStatus", + VALUES : { + "0x01" : "Current reset count is less than the reset amount", + "0x02" : "Current reset count exceeds the reset amount", + }, + }, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +function decodeBasicInformation(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_INFO.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_INFO.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + // No channel checking + // Type of basic information + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + var info = CONFIG_INFO.TYPES[type]; + size = info.SIZE; + // Decoding + var value = 0; + if(size != 0) + { + if(info.DIGIT || info.DIGIT == false) + { + if(info.DIGIT == false) + { + // Decode into "V" + DIGIT STRING + "." DIGIT STRING format + value = getDigitStringArrayNoFormat(bytes, index, size); + value = "V" + value[0] + "." + value[1]; + }else + { + // Decode into DIGIT STRING format + value = getDigitStringArrayEvenFormat(bytes, index, size); + value = value.toString(); + } + } + else if(info.VALUES) + { + // Decode into STRING (VALUES specified in CONFIG_INFO) + value = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + value = info.VALUES[value]; + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + if(info.RESOLUTION) + { + value = value * info.RESOLUTION; + value = parseFloat(value.toFixed(2)); + } + if(info.UNIT) + { + decoded[info.NAME] = {}; + decoded[info.NAME]["data"] = value; + decoded[info.NAME]["unit"] = info.UNIT; + // decoded[info.NAME] = value; + }else + { + decoded[info.NAME] = value; + } + index = index + size; + }else + { + // Device Model (End of decoding) + size = LENGTH - index; + decoded[info.NAME] = getStringFromBytesBigEndianFormat(bytes, index, size); + index = index + size; + } + } + }catch(error) + { + decoded[CONFIG_INFO.ERROR_NAME] = error.message; + } + + return decoded; +} + +function decodeDeviceData(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_MEASUREMENT.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_MEASUREMENT.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + // Type of device measurement + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + + // channel checking + if(channel == 11 && type == "0x0A") + { + // Modbus error code decoding + decoded.modbusErrorCode = bytes[index]; + index = index + 1; + continue; // next channel + } + + var measurement = CONFIG_MEASUREMENT.TYPES[type]; + size = measurement.SIZE; + // Decoding + var value = 0; + if(measurement.DIGIT || measurement.DIGIT == false) + { + if(measurement.DIGIT == false) + { + // Decode into "V" + DIGIT STRING + "." DIGIT STRING format + value = getDigitStringArrayNoFormat(bytes, index, size); + value = "V" + value[0] + "." + value[1]; + }else + { + // Decode into DIGIT NUMBER format + value = getDigitStringArrayEvenFormat(bytes, index, size); + value = parseInt(value.join("")); + } + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + if(measurement.SIGNED) + { + value = getSignedIntegerFromInteger(value, size); + } + if(measurement.RESOLUTION) + { + value = value * measurement.RESOLUTION; + value = parseFloat(value.toFixed(2)); + } + if(measurement.UNIT) + { + decoded[measurement.NAME] = {}; + decoded[measurement.NAME]["data"] = value; + decoded[measurement.NAME]["unit"] = measurement.UNIT; + // decoded[measurement.NAME] = value; + }else + { + decoded[measurement.NAME] = value; + } + index = index + size; + + } + }catch(error) + { + decoded[CONFIG_MEASUREMENT.ERROR_NAME] = error.message; + } + return decoded; +} + +function decodeChangeState(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_STATE.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_STATE.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + // Type of change of state + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + // No channel checking + var state = CONFIG_STATE.TYPES[type]; + size = state.SIZE; + // Decoding + var value = 0; + if(size != 0) + { + if(state.VALUES) + { + // Decode into STRING (VALUES specified in CONFIG_STATE) + value = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + value = state.VALUES[value]; + } + index = index + size; + } + decoded[state.NAME] = value; + } + }catch(error) + { + decoded[CONFIG_STATE.ERROR_NAME] = error.message; + } + + return decoded; +} + +function decodeEventLogging(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_LOGGING.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_LOGGING.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + // Type of change of state + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + // No channel checking + var logging = CONFIG_LOGGING.TYPES[type]; + size = logging.SIZE; + // Decoding + var value = 0; + if(size != 0) + { + if(logging.VALUES) + { + // Decode into STRING (VALUES specified in CONFIG_LOGGING) + value = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + value = logging.VALUES[value]; + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + index = index + size; + } + decoded[logging.NAME] = value; + } + }catch(error) + { + decoded[CONFIG_LOGGING.ERROR_NAME] = error.message; + } + + return decoded; +} + +function getStringFromBytesBigEndianFormat(bytes, index, size) +{ + var value = ""; + for(var i=0; i=0; i=i-1) + { + value = value + String.fromCharCode(bytes[index+i]); + } + return value; +} + +function getValueFromBytesBigEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=0; i<(size-1); i=i+1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index+size-1] + return (value >>> 0); // to unsigned +} + +function getValueFromBytesLittleEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=(size-1); i>0; i=i-1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index] + return (value >>> 0); // to unsigned +} + +function getDigitStringArrayNoFormat(bytes, index, size) +{ + var hexString = [] + for(var i=0; i= CONFIG_MEASUREMENT.FPORT_MIN && fPort <= CONFIG_MEASUREMENT.FPORT_MAX) + { + decoded = decodeDeviceData(bytes); + }else if(fPort == CONFIG_STATE.FPORT) + { + decoded = decodeChangeState(bytes); + }else if(fPort == CONFIG_LOGGING.FPORT) + { + decoded = decodeEventLogging(bytes); + }else + { + decoded = {error: "Incorrect fPort", fPort : fPort}; + } + decoded[VERSION_CONTROL.CODEC.NAME] = VERSION_CONTROL.CODEC.VERSION; + decoded[VERSION_CONTROL.DEVICE.NAME] = VERSION_CONTROL.DEVICE.MODEL; + decoded[VERSION_CONTROL.PRODUCT.NAME] = VERSION_CONTROL.PRODUCT.CODE; + decoded[VERSION_CONTROL.MANUFACTURER.NAME] = VERSION_CONTROL.MANUFACTURER.COMPANY; + return decoded; +} + +// Decode uplink function. (ChirpStack v4 , TTN) +// +// Input is an object with the following fields: +// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0] +// - fPort = Uplink fPort. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - data = Object representing the decoded payload. +function decodeUplink(input) { + return { + data: Decode(input.fPort, input.bytes, input.variables) + }; +} + +/************************************************************************************************************/ + +// Encode encodes the given object into an array of bytes. (ChirpStack v3) +// - fPort contains the LoRaWAN fPort number +// - obj is an object, e.g. {"temperature": 22.5} +// - variables contains the device variables e.g. {"calibration": "3.5"} (both the key / value are of type string) +// The function must return an array of bytes, e.g. [225, 230, 255, 0] +function Encode(fPort, obj, variables) { + try + { + if(obj[CONFIG_DOWNLINK.TYPE] == CONFIG_DOWNLINK.CONFIG) + { + return encodeDeviceConfiguration(obj[CONFIG_DOWNLINK.CONFIG], variables); + } + else if(obj[CONFIG_DOWNLINK.TYPE] == CONFIG_DOWNLINK.DYNAMIC) + { + return encodeDynamicLimitControl(obj[CONFIG_DOWNLINK.DYNAMIC], variables); + } + else if(obj[CONFIG_DOWNLINK.TYPE] == CONFIG_DOWNLINK.RELAY) + { + return encodeRelayControl(obj[CONFIG_DOWNLINK.RELAY], variables); + } + else if(obj[CONFIG_DOWNLINK.TYPE] == CONFIG_DOWNLINK.MEASURE) + { + return encodePeriodicPackage(obj[CONFIG_DOWNLINK.MEASURE], variables); + } + else if(obj[CONFIG_DOWNLINK.TYPE] == CONFIG_DOWNLINK.REQUEST) + { + return encodeRequestSettings(obj[CONFIG_DOWNLINK.REQUEST], variables); + } + }catch(error) + { + + } + return []; +} + +// Encode downlink function. (ChirpStack v4 , TTN) +// +// Input is an object with the following fields: +// - data = Object representing the payload that must be encoded. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - bytes = Byte array containing the downlink payload. +function encodeDownlink(input) { + return { + bytes: Encode(null, input.data, input.variables) + }; +} + +/************************************************************************************************************/ + +// Constants for device configuration +var CONFIG_DEVICE = { + FPORT : 50, + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "restart" : {TYPE : parseInt("0x0B", 16), SIZE : 1, MIN : 1, MAX : 1,}, + "digitalInput" : {TYPE : parseInt("0x47", 16), SIZE : 1, MIN : 0, MAX : 1, CHANNEL : parseInt("0x08", 16),}, + "currentLimitFallback" : {TYPE : parseInt("0x32", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "voltageLimitFallback" : {TYPE : parseInt("0x33", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "powerLimitFallback" : {TYPE : parseInt("0x34", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "deactivationDelayFallback" : {TYPE : parseInt("0x35", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "activationDelayFallback" : {TYPE : parseInt("0x36", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "offsetCurrentFallback" : {TYPE : parseInt("0x37", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "offsetDelayFallback" : {TYPE : parseInt("0x38", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "resetTimeFallback" : {TYPE : parseInt("0x39", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "resetAmountFallback" : {TYPE : parseInt("0x3A", 16), SIZE : 1, MIN : 0, MAX : 255,} + } +} + +// Constants for Dynamic limit control +var CONFIG_DYNAMIC = { + FPORT : 50, + CHANNEL : parseInt("0x01", 16), + TYPES : { + "currentLimit" : {TYPE : parseInt("0x32", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "voltageLimit" : {TYPE : parseInt("0x33", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "powerLimit" : {TYPE : parseInt("0x34", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "deactivationDelay" : {TYPE : parseInt("0x35", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "activationDelay" : {TYPE : parseInt("0x36", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "offsetCurrent" : {TYPE : parseInt("0x37", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "offsetDelay" : {TYPE : parseInt("0x38", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "resetTime" : {TYPE : parseInt("0x39", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "resetAmount" : {TYPE : parseInt("0x3A", 16), SIZE : 1, MIN : 0, MAX : 255,} + } +} + +// Constants for Relay control +var CONFIG_RELAY = { + FPORT : 50, + CHANNEL : parseInt("0x07", 16), + TYPES : { + "reset" : {TYPE : parseInt("0x46", 16), SIZE : 1, MIN : 1, MAX : 1,}, + "controlMode" : {TYPE : parseInt("0x47", 16), SIZE : 1, MIN : 0, MAX : 1,}, + "relayCommand" : {TYPE : parseInt("0x48", 16), SIZE : 1, MIN : 0, MAX : 1,} + } +} + +// Constants for device periodic package +var CONFIG_PERIODIC = { + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "Interval" : {TYPE : parseInt("0x14", 16), SIZE : 1, MIN : 1, MAX : 255,}, + "Mode" : {TYPE : parseInt("0x15", 16), SIZE : 1, MIN : 0, MAX : 1,}, + "Status" : {TYPE : parseInt("0x16", 16), SIZE : 1, MIN : 0, MAX : 1,}, + "Measurement" : {TYPE : parseInt("0x17", 16), SIZE : 1, MIN : 0, MAX : 10,}, + }, + MEASUREMENTS : { + index : "0x00", + timestamp : "0x01", + dataloggerTimestamp : "0x03", + activeEnergyImportL1T1 : "0x04", + activeEnergyImportL1T2 : "0x05", + activeEnergyExportL1T1 : "0x06", + activeEnergyExportL1T2 : "0x07", + reactiveEnergyImportL1T1 : "0x08", + reactiveEnergyImportL1T2 : "0x09", + reactiveEnergyExportL1T1 : "0x0A", + reactiveEnergyExportL1T2 : "0x0B", + voltageL1N : "0x0C", + currentL1 : "0x10", + activePowerL1 : "0x14", + reactivePowerL1 : "0x17", + apparentPowerL1 : "0x1A", + powerFactorL1 : "0x1d", + phaseAngleL1 : "0x20", + frequency : "0x23", + totalSystemActivePower : "0x24", + totalSystemReactivePower : "0x25", + totalSystemApparentPower : "0x26", + maximumL1CurrentDemand : "0x27", + AveragePower : "0x2A", + midYearOfCertification : "0x2B", + manufacturedYear : "0xF0", + firmwareVersion : "0xF1", + hardwareVersion : "0xF2", + } +} + +// Constants for request settings +var CONFIG_REQUEST = { + FPORT: 50, + CHANNEL : parseInt("0x02", 16), + TYPE : parseInt("0x0B", 16), + MIN: 1, + MAX: 10, + SETTINGS : { + currentLimitFallback : "0x3C", + voltageLimitFallback : "0x3D", + powerLimitFallback : "0x3E", + deactivationDelayFallback : "0x3F", + activationDelayFallback : "0x40", + offsetCurrentFallback : "0x41", + offsetDelayFallback : "0x42", + resetTimeFallback : "0x43", + resetAmountFallback : "0x44", + currentLimitDynamic : "0x50", + voltageLimitDynamic : "0x51", + powerLimitDynamic : "0x52", + deactivationDelayDynamic : "0x53", + activationDelayDynamic : "0x54", + offsetCurrentDynamic : "0x55", + offsetDelayDynamic : "0x56", + resetTimeDynamic : "0x57", + resetAmountDynamic : "0x58", + } + +} + +// Constants for downlink type (Config or Measure) +var CONFIG_DOWNLINK = { + TYPE : "Type", + CONFIG : "Config", + DYNAMIC : "Dynamic", + RELAY : "Relay", + MEASURE : "Measure", + REQUEST : "RequestSettings" +} + +function encodeDeviceConfiguration(obj, variables) +{ + var encoded = [] + var index = 0; + var field = ["Param", "Value"]; + try + { + var config = CONFIG_DEVICE.TYPES[obj[field[0]]]; + var value = obj[field[1]]; + if(obj[field[1]] >= config.MIN && obj[field[1]] <= config.MAX) + { + if("CHANNEL" in config) + { + encoded[index] = config.CHANNEL; + }else + { + encoded[index] = CONFIG_DEVICE.CHANNEL; + } + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + for(var i=1; i<=config.SIZE; i=i+1) + { + encoded[index] = (value >> 8*(config.SIZE - i)) % 256; + index = index + 1; + } + }else + { + // Error + return []; + } + }catch(error) + { + // Error + return []; + } + return encoded; +} + +function encodeDynamicLimitControl(obj, variables) +{ + var encoded = [] + var index = 0; + var field = ["Param", "Value"]; + try + { + var config = CONFIG_DYNAMIC.TYPES[obj[field[0]]]; + var value = obj[field[1]]; + if(obj[field[1]] >= config.MIN && obj[field[1]] <= config.MAX) + { + encoded[index] = CONFIG_DYNAMIC.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + for(var i=1; i<=config.SIZE; i=i+1) + { + encoded[index] = (value >> 8*(config.SIZE - i)) % 256; + index = index + 1; + } + }else + { + // Error + return []; + } + }catch(error) + { + // Error + return []; + } + return encoded; +} + +function encodeRelayControl(obj, variables) +{ + var encoded = [] + var index = 0; + var field = ["Param", "Value"]; + try + { + var config = CONFIG_RELAY.TYPES[obj[field[0]]]; + var value = obj[field[1]]; + if(obj[field[1]] >= config.MIN && obj[field[1]] <= config.MAX) + { + encoded[index] = CONFIG_RELAY.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + for(var i=1; i<=config.SIZE; i=i+1) + { + encoded[index] = (value >> 8*(config.SIZE - i)) % 256; + index = index + 1; + } + }else + { + // Error + return []; + } + }catch(error) + { + // Error + return []; + } + return encoded; +} + +function encodePeriodicPackage(obj, variables) +{ + var encoded = [] + var index = 0; + var field = ["Interval", "Mode", "Status", "Measurement"]; + try + { + // Encode Interval, Mode, Status + for(var i=0; i<3; i=i+1) + { + if(field[i] in obj) + { + var config = CONFIG_PERIODIC.TYPES[field[i]]; + if(obj[field[i]] >= config.MIN && obj[field[i]] <= config.MAX) + { + encoded[index] = CONFIG_PERIODIC.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + encoded[index] = obj[field[i]]; + index = index + 1; + }else + { + // Error + return []; + } + } + } + // Encode Measurement + if(field[3] in obj) + { + var measurements = obj[field[3]]; + var LENGTH = measurements.length; + var config = CONFIG_PERIODIC.TYPES[field[3]]; + if(LENGTH > config.MAX) + { + // Error + return []; + } + var measurement = ""; + if(LENGTH > 0) + { + encoded[index] = CONFIG_PERIODIC.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + } + for(var i=0; i Date: Fri, 20 Mar 2026 21:41:21 +0100 Subject: [PATCH 2/5] Add YOBIIQ EM4301 --- vendors/yobiiq/codecs/em4301.js | 641 ++++++++++++++++++ vendors/yobiiq/codecs/test_decode_em4301.json | 228 +++++++ vendors/yobiiq/codecs/test_encode_em4301.json | 108 +++ vendors/yobiiq/devices/yobiiq-em2101.toml | 2 +- vendors/yobiiq/devices/yobiiq-em4301.toml | 35 + 5 files changed, 1013 insertions(+), 1 deletion(-) create mode 100644 vendors/yobiiq/codecs/em4301.js create mode 100644 vendors/yobiiq/codecs/test_decode_em4301.json create mode 100644 vendors/yobiiq/codecs/test_encode_em4301.json create mode 100644 vendors/yobiiq/devices/yobiiq-em4301.toml diff --git a/vendors/yobiiq/codecs/em4301.js b/vendors/yobiiq/codecs/em4301.js new file mode 100644 index 0000000..0a312be --- /dev/null +++ b/vendors/yobiiq/codecs/em4301.js @@ -0,0 +1,641 @@ +/** + * Codec for EM4301 device : compatible with TTN, ChirpStack v4 and v3, etc... + * Release Date : 11 June 2023 + * Update Date : 05 November 2024 + */ + +// Version Control +var VERSION_CONTROL = { + CODEC : {VERSION: "1.0.1", NAME: "codecVersion"}, + DEVICE: {MODEL : "EM4301", NAME: "genericModel"}, + PRODUCT: {CODE : "P1002011", NAME: "productCode"}, + MANUFACTURER: {COMPANY : "YOBIIQ B.V.", NAME: "manufacturer"}, +} + + +// Configuration constants for device basic info +var CONFIG_INFO = { + FPORT : 50, + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "0x09" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false}, + "0x0A" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false}, + "0x16" : {SIZE : 4, NAME : "deviceSerialNumber"}, + "0x0F" : {SIZE : 1, NAME : "deviceClass", + VALUES : { + "0x00" : "Class A", + "0x01" : "Class B", + "0x02" : "Class C", + }, + }, + "0x0B" : {SIZE : 1, NAME : "powerEvent", + VALUES : { + "0x00" : "AC Power Off", + "0x01" : "AC Power On", + }, + }, + "0x1E" : {SIZE : 2, NAME : "primaryCurrentTransformerRatio",}, + "0x1F" : {SIZE : 1, NAME : "secondaryCurrentTransformerRatio",}, + "0x20" : {SIZE : 4, NAME : "primaryVoltageTransformerRatio",}, + "0x21" : {SIZE : 2, NAME : "secondaryVoltageTransformerRatio",}, + "0x28" : {SIZE : 0, NAME : "deviceModel",}, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +// Configuration constants for measurement registers + var CONFIG_MEASUREMENT = { + FPORT_MIN : 1, + FPORT_MAX : 10, + TYPES : { + "0x00" : {SIZE : 4, NAME : "index",}, + "0x01" : {SIZE : 4, NAME : "timestamp",}, + "0x03" : {SIZE : 4, NAME : "dataloggerTimestamp",}, + "0x04" : {SIZE : 4, NAME : "activeEnergyImportL123T1", UNIT : "Wh",}, + "0x05" : {SIZE : 4, NAME : "activeEnergyImportL123T2", UNIT : "Wh",}, + "0x06" : {SIZE : 4, NAME : "activeEnergyExportL123T1", UNIT : "Wh",}, + "0x07" : {SIZE : 4, NAME : "activeEnergyExportL123T2", UNIT : "Wh",}, + "0x08" : {SIZE : 4, NAME : "reactiveEnergyImportL123T1", UNIT : "varh",}, + "0x09" : {SIZE : 4, NAME : "reactiveEnergyImportL123T2", UNIT : "varh",}, + "0x0A" : {SIZE : 4, NAME : "reactiveEnergyExportL123T1", UNIT : "varh",}, + "0x0B" : {SIZE : 4, NAME : "reactiveEnergyExportL123T2", UNIT : "varh",}, + "0x0C" : {SIZE : 4, NAME : "voltageL1N", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x0D" : {SIZE : 4, NAME : "voltageL2N", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x0E" : {SIZE : 4, NAME : "voltageL3N", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x0F" : {SIZE : 4, NAME : "currentL123", UNIT : "mA", SIGNED : true,}, + "0x10" : {SIZE : 4, NAME : "currentL1", UNIT : "mA", SIGNED : true,}, + "0x11" : {SIZE : 4, NAME : "currentL2", UNIT : "mA", SIGNED : true,}, + "0x12" : {SIZE : 4, NAME : "currentL3", UNIT : "mA", SIGNED : true,}, + "0x13" : {SIZE : 4, NAME : "activePowerL123", UNIT : "W", SIGNED : true,}, + "0x14" : {SIZE : 4, NAME : "activePowerL1", UNIT : "W", SIGNED : true,}, + "0x15" : {SIZE : 4, NAME : "activePowerL2", UNIT : "W", SIGNED : true,}, + "0x16" : {SIZE : 4, NAME : "activePowerL3", UNIT : "W", SIGNED : true,}, + "0x17" : {SIZE : 4, NAME : "reactivePowerL1", UNIT : "kvar", RESOLUTION : 0.001, SIGNED : true,}, + "0x18" : {SIZE : 4, NAME : "reactivePowerL2", UNIT : "kvar", RESOLUTION : 0.001, SIGNED : true,}, + "0x19" : {SIZE : 4, NAME : "reactivePowerL3", UNIT : "kvar", RESOLUTION : 0.001, SIGNED : true,}, + "0x1A" : {SIZE : 4, NAME : "apparentPowerL1", UNIT : "kVA", RESOLUTION : 0.001, SIGNED : true,}, + "0x1B" : {SIZE : 4, NAME : "apparentPowerL2", UNIT : "kVA", RESOLUTION : 0.001, SIGNED : true,}, + "0x1C" : {SIZE : 4, NAME : "apparentPowerL3", UNIT : "kVA", RESOLUTION : 0.001, SIGNED : true,}, + "0x1D" : {SIZE : 1, NAME : "powerFactorL1", RESOLUTION : 0.01, SIGNED : true,}, + "0x1E" : {SIZE : 1, NAME : "powerFactorL2", RESOLUTION : 0.01, SIGNED : true,}, + "0x1F" : {SIZE : 1, NAME : "powerFactorL3", RESOLUTION : 0.01, SIGNED : true,}, + "0x20" : {SIZE : 2, NAME : "phaseAngleL1", UNIT : "degree", RESOLUTION : 0.01, SIGNED : true,}, + "0x21" : {SIZE : 2, NAME : "phaseAngleL2", UNIT : "degree", RESOLUTION : 0.01, SIGNED : true,}, + "0x22" : {SIZE : 2, NAME : "phaseAngleL3", UNIT : "degree", RESOLUTION : 0.01, SIGNED : true,}, + "0x23" : {SIZE : 2, NAME : "frequency", UNIT : "Hz", RESOLUTION : 0.01, SIGNED : true,}, + "0x24" : {SIZE : 4, NAME : "totalSystemActivePower", UNIT : "kW",}, + "0x25" : {SIZE : 4, NAME : "totalSystemReactivePower", UNIT : "kvar", RESOLUTION : 0.001,}, + "0x26" : {SIZE : 4, NAME : "totalSystemApparentPower", UNIT : "kVA", RESOLUTION : 0.001,}, + "0x27" : {SIZE : 4, NAME : "maximumL1CurrentDemand", UNIT : "mA", SIGNED : true,}, + "0x28" : {SIZE : 4, NAME : "maximumL2CurrentDemand", UNIT : "mA", SIGNED : true,}, + "0x29" : {SIZE : 4, NAME : "maximumL3CurrentDemand", UNIT : "mA", SIGNED : true,}, + "0x2A" : {SIZE : 4, NAME : "averagePower", UNIT : "W", SIGNED : true,}, + "0x2B" : {SIZE : 4, NAME : "midYearOfCertification",}, + "0xF0" : {SIZE : 2, NAME : "manufacturedYear", DIGIT: true,}, + "0xF1" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false,}, + "0xF2" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false,}, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +function decodeBasicInformation(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_INFO.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_INFO.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + if(channel == CONFIG_INFO.CHANNEL) + { + // Type of basic information + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + var info = CONFIG_INFO.TYPES[type]; + size = info.SIZE; + // Decoding + var value = 0; + if(size != 0) + { + if(info.DIGIT || info.DIGIT == false) + { + if(info.DIGIT == false) + { + // Decode into "V" + DIGIT STRING + "." DIGIT STRING format + value = getDigitStringArrayNoFormat(bytes, index, size); + value = "V" + value[0] + "." + value[1]; + }else + { + // Decode into DIGIT STRING format + value = getDigitStringArrayEvenFormat(bytes, index, size); + value = value.toString(); + } + }else + { + if(info.VALUES) + { + // Decode into STRING (VALUES specified in CONFIG_INFO) + value = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + value = info.VALUES[value]; + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + } + decoded[info.NAME] = value; + index = index + size; + }else + { + // Device Model (End of decoding) + size = getSizeBasedOnChannel(bytes, index, channel); + decoded[info.NAME] = getStringFromBytesBigEndianFormat(bytes, index, size); + index = index + size; + } + } + } + }catch(error) + { + decoded[CONFIG_INFO.ERROR_NAME] = error.message; + } + + return decoded; +} + +function decodeDeviceData(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_MEASUREMENT.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_MEASUREMENT.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + // Type of device measurement + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + + // channel checking + if(channel == 11 && type == "0x0A") + { + // Modbus error code decoding + decoded.modbusErrorCode = bytes[index]; + index = index + 1; + continue; // next channel + } + + var measurement = CONFIG_MEASUREMENT.TYPES[type]; + size = measurement.SIZE; + // Decoding + var value = 0; + if(measurement.DIGIT || measurement.DIGIT == false) + { + if(measurement.DIGIT == false) + { + // Decode into "V" + DIGIT STRING + "." DIGIT STRING format + value = getDigitStringArrayNoFormat(bytes, index, size); + value = "V" + value[0] + "." + value[1]; + }else + { + // Decode into DIGIT NUMBER format + value = getDigitStringArrayEvenFormat(bytes, index, size); + value = parseInt(value.join("")); + } + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + if(measurement.SIGNED) + { + value = getSignedIntegerFromInteger(value, size); + } + if(measurement.RESOLUTION) + { + value = value * measurement.RESOLUTION; + value = parseFloat(value.toFixed(2)); + } + if(measurement.UNIT) + { + decoded[measurement.NAME] = {}; + decoded[measurement.NAME]["data"] = value; + decoded[measurement.NAME]["unit"] = measurement.UNIT; + // decoded[measurement.NAME] = value; + }else + { + decoded[measurement.NAME] = value; + } + index = index + size; + + } + }catch(error) + { + decoded[CONFIG_MEASUREMENT.ERROR_NAME] = error.message; + } + return decoded; +} + +function getStringFromBytesBigEndianFormat(bytes, index, size) +{ + var value = ""; + for(var i=0; i=0; i=i-1) + { + value = value + String.fromCharCode(bytes[index+i]); + } + return value; +} + +function getValueFromBytesBigEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=0; i<(size-1); i=i+1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index+size-1] + return (value >>> 0); // to unsigned +} + +function getValueFromBytesLittleEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=(size-1); i>0; i=i-1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index] + return (value >>> 0); // to unsigned +} + +function getDigitStringArrayNoFormat(bytes, index, size) +{ + var hexString = [] + for(var i=0; i= CONFIG_MEASUREMENT.FPORT_MIN && fPort <= CONFIG_MEASUREMENT.FPORT_MAX) + { + decoded = decodeDeviceData(bytes); + }else + { + decoded = {error: "Incorrect fPort", fPort : fPort}; + } + decoded[VERSION_CONTROL.CODEC.NAME] = VERSION_CONTROL.CODEC.VERSION; + decoded[VERSION_CONTROL.DEVICE.NAME] = VERSION_CONTROL.DEVICE.MODEL; + decoded[VERSION_CONTROL.PRODUCT.NAME] = VERSION_CONTROL.PRODUCT.CODE; + decoded[VERSION_CONTROL.MANUFACTURER.NAME] = VERSION_CONTROL.MANUFACTURER.COMPANY; + return decoded; +} + +// Decode uplink function. (ChirpStack v4 , TTN) +// +// Input is an object with the following fields: +// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0] +// - fPort = Uplink fPort. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - data = Object representing the decoded payload. +function decodeUplink(input) { + return { + data: Decode(input.fPort, input.bytes, input.variables) + }; +} + +/************************************************************************************************************/ + +// Encode encodes the given object into an array of bytes. (ChirpStack v3) +// - fPort contains the LoRaWAN fPort number +// - obj is an object, e.g. {"temperature": 22.5} +// - variables contains the device variables e.g. {"calibration": "3.5"} (both the key / value are of type string) +// The function must return an array of bytes, e.g. [225, 230, 255, 0] +function Encode(fPort, obj, variables) { + try + { + if(obj[CONFIG_DOWNLINK.TYPE] == CONFIG_DOWNLINK.CONFIG) + { + return encodeDeviceConfiguration(obj[CONFIG_DOWNLINK.CONFIG], variables); + }else if(obj[CONFIG_DOWNLINK.TYPE] == CONFIG_DOWNLINK.MEASURE) + { + return encodePeriodicPackage(obj[[CONFIG_DOWNLINK.MEASURE]], variables); + } + }catch(error) + { + + } + return []; +} + +// Encode downlink function. (ChirpStack v4 , TTN) +// +// Input is an object with the following fields: +// - data = Object representing the payload that must be encoded. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - bytes = Byte array containing the downlink payload. +function encodeDownlink(input) { + return { + bytes: Encode(null, input.data, input.variables) + }; +} + +/************************************************************************************************************/ + +// Constants for device configuration +var CONFIG_DEVICE = { + FPORT : 50, + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "restart" : {TYPE : parseInt("0x0B", 16), SIZE : 1, MIN : 1, MAX : 1,}, + "primaryCurrentTransformerRatio" : {TYPE : parseInt("0x1E", 16), SIZE : 2, MIN : 0, MAX : 9999,}, + "secondaryCurrentTransformerRatio" : {TYPE : parseInt("0x1F", 16), SIZE : 1, MIN : 0, MAX : 5,}, + "primaryVoltageTransformerRatio" : {TYPE : parseInt("0x20", 16), SIZE : 4, MIN : 30, MAX : 500000,}, + "secondaryVoltageTransformerRatio" : {TYPE : parseInt("0x21", 16), SIZE : 2, MIN : 30, MAX : 500,}, + } +} + +// Constants for device periodic package +var CONFIG_PERIODIC = { + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "Interval" : {TYPE : parseInt("0x14", 16), SIZE : 1, MIN : 1, MAX : 255,}, + "Mode" : {TYPE : parseInt("0x15", 16), SIZE : 1, MIN : 0, MAX : 1,}, + "Status" : {TYPE : parseInt("0x16", 16), SIZE : 1, MIN : 0, MAX : 1,}, + "Measurement" : {TYPE : parseInt("0x17", 16), SIZE : 1, MIN : 0, MAX : 10,}, + }, + MEASUREMENTS : { + index : "0x00", + timestamp : "0x01", + dataloggerTimestamp : "0x03", + activeEnergyImportL123T1 : "0x04", + activeEnergyImportL123T2 : "0x05", + activeEnergyExportL123T1 : "0x06", + activeEnergyExportL123T2 : "0x07", + reactiveEnergyImportL123T1 : "0x08", + reactiveEnergyImportL123T2 : "0x09", + reactiveEnergyExportL123T1 : "0x0A", + reactiveEnergyExportL123T2 : "0x0B", + voltageL1N : "0x0C", + voltageL2N : "0x0D", + voltageL3N : "0x0E", + currentL123 : "0x0F", + currentL1 : "0x10", + currentL2 : "0x11", + currentL3 : "0x12", + activePowerL123 : "0x13", + activePowerL1 : "0x14", + activePowerL2 : "0x15", + activePowerL3 : "0x16", + reactivePowerL1 : "0x17", + reactivePowerL2 : "0x18", + reactivePowerL3 : "0x19", + apparentPowerL1 : "0x1A", + apparentPowerL2 : "0x1B", + apparentPowerL3 : "0x1C", + powerFactorL1 : "0x1D", + powerFactorL2 : "0x1E", + powerFactorL3 : "0x1F", + phaseAngleL1 : "0x20", + phaseAngleL2 : "0x21", + phaseAngleL3 : "0x22", + frequency : "0x23", + totalSystemActivePower : "0x24", + totalSystemReactivePower : "0x25", + totalSystemApparentPower : "0x26", + maximumL1CurrentDemand : "0x27", + maximumL2CurrentDemand : "0x28", + maximumL3CurrentDemand : "0x29", + averagePower : "0x2A", + midYearOfCertification : "0x2B", + manufacturedYear : "0xF0", + firmwareVersion : "0xF1", + hardwareVersion : "0xF2", + } +} + +// Constants for downlink type (Config or Measure) +var CONFIG_DOWNLINK = { + TYPE : "Type", + CONFIG : "Config", + MEASURE : "Measure", +} + +function encodeDeviceConfiguration(obj, variables) +{ + var encoded = [] + var index = 0; + var field = ["Param", "Value"]; + try + { + var config = CONFIG_DEVICE.TYPES[obj[field[0]]]; + var value = obj[field[1]]; + if(obj[field[1]] >= config.MIN && obj[field[1]] <= config.MAX) + { + encoded[index] = CONFIG_DEVICE.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + for(var i=1; i<=config.SIZE; i=i+1) + { + encoded[index] = (value >> 8*(config.SIZE - i)) % 256; + index = index + 1; + } + }else + { + // Error + return []; + } + }catch(error) + { + // Error + return []; + } + return encoded; +} + +function encodePeriodicPackage(obj, variables) +{ + var encoded = [] + var index = 0; + var field = ["Interval", "Mode", "Status", "Measurement"]; + try + { + // Encode Interval, Mode, Status + for(var i=0; i<3; i=i+1) + { + if(field[i] in obj) + { + var config = CONFIG_PERIODIC.TYPES[field[i]]; + if(obj[field[i]] >= config.MIN && obj[field[i]] <= config.MAX) + { + encoded[index] = CONFIG_PERIODIC.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + encoded[index] = obj[field[i]]; + index = index + 1; + }else + { + // Error + return []; + } + } + } + + // Encode Measurement + if(field[3] in obj) + { + var measurements = obj[field[3]]; + var LENGTH = measurements.length; + var config = CONFIG_PERIODIC.TYPES[field[3]]; + if(LENGTH > config.MAX) + { + // Error + return []; + } + var measurement = ""; + if(LENGTH > 0) + { + encoded[index] = CONFIG_PERIODIC.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + } + for(var i=0; i Date: Sat, 21 Mar 2026 00:22:06 +0100 Subject: [PATCH 3/5] Add YOBIIQ EM4301-CT --- vendors/yobiiq/devices/yobiiq-em4301-ct.toml | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 vendors/yobiiq/devices/yobiiq-em4301-ct.toml diff --git a/vendors/yobiiq/devices/yobiiq-em4301-ct.toml b/vendors/yobiiq/devices/yobiiq-em4301-ct.toml new file mode 100644 index 0000000..a770b2e --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-em4301-ct.toml @@ -0,0 +1,35 @@ +[device] + # Device name. + name = "YOBIIQ EM4301-CT" + + # Device description. + description = "EM4301-CT is a 3phase Electricity Meter with measurement through current transformers" + + # Device metadata (optional). + [device.metadata] + # Product URL. + product_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-ct-electricity-meter/" + + # Documentation URL. + documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-ct-electricity-meter/" + + + # Device firmware version. + # + # This section can be repeated in case multiple firmware versions exist. + # As a new firmware version can change the supported profiles / regions and + # payload format, each firmware version has its own profiles and codec + # configuration. + [[device.firmware]] + # Firmware version. + version = "1.0.0" + + # List of supported profiles. + # + # This list refers to one or multiple profiles in the profiles/ directory. + profiles = ["EU868-classC.toml"] + + # Payload codec. + # + # In case no codec is available, you can remove this option. + codec = "em4301.js" \ No newline at end of file From a929e13072fed4c8a63beaa9f2f25cf233b3bf56 Mon Sep 17 00:00:00 2001 From: Dominic Hakke Date: Sat, 21 Mar 2026 20:54:38 +0100 Subject: [PATCH 4/5] Remove leading zero from Vendor ID --- vendors/yobiiq/vendor.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendors/yobiiq/vendor.toml b/vendors/yobiiq/vendor.toml index 3dd3dda..6162c46 100644 --- a/vendors/yobiiq/vendor.toml +++ b/vendors/yobiiq/vendor.toml @@ -6,7 +6,7 @@ # # See also: # ttps://resources.lora-alliance.org/document/lora-alliance-vendor-id-20230915 - vendor_id = 0415 + vendor_id = 415 # OUIs owned by vendor (optional). # From 137fa15e3f91b86642899ade0d18f62302946221 Mon Sep 17 00:00:00 2001 From: Dominic Hakke Date: Sat, 21 Mar 2026 21:28:59 +0100 Subject: [PATCH 5/5] Adding YOBIIQ Devices - first section --- vendors/yobiiq/codecs/dsmr.js | 1357 +++++++++++++++++ vendors/yobiiq/codecs/sd1001.js | 454 ++++++ vendors/yobiiq/codecs/test_decode_dsmr.json | 166 ++ vendors/yobiiq/codecs/test_decode_sd1001.json | 90 ++ vendors/yobiiq/codecs/test_encode_dsmr.json | 99 ++ vendors/yobiiq/codecs/test_encode_sd1001.json | 62 + vendors/yobiiq/devices/yobiiq-dsmr.toml | 13 + vendors/yobiiq/devices/yobiiq-em2101.toml | 46 +- vendors/yobiiq/devices/yobiiq-em4301-ct.toml | 46 +- vendors/yobiiq/devices/yobiiq-em4301.toml | 46 +- vendors/yobiiq/devices/yobiiq-sd1001-ac.toml | 13 + vendors/yobiiq/devices/yobiiq-sd1001-dc.toml | 13 + vendors/yobiiq/profiles/EU868-classA.toml | 116 +- vendors/yobiiq/profiles/EU868-classC.toml | 116 +- vendors/yobiiq/vendor.toml | 28 +- 15 files changed, 2357 insertions(+), 308 deletions(-) create mode 100644 vendors/yobiiq/codecs/dsmr.js create mode 100644 vendors/yobiiq/codecs/sd1001.js create mode 100644 vendors/yobiiq/codecs/test_decode_dsmr.json create mode 100644 vendors/yobiiq/codecs/test_decode_sd1001.json create mode 100644 vendors/yobiiq/codecs/test_encode_dsmr.json create mode 100644 vendors/yobiiq/codecs/test_encode_sd1001.json create mode 100644 vendors/yobiiq/devices/yobiiq-dsmr.toml create mode 100644 vendors/yobiiq/devices/yobiiq-sd1001-ac.toml create mode 100644 vendors/yobiiq/devices/yobiiq-sd1001-dc.toml diff --git a/vendors/yobiiq/codecs/dsmr.js b/vendors/yobiiq/codecs/dsmr.js new file mode 100644 index 0000000..f1a0cd1 --- /dev/null +++ b/vendors/yobiiq/codecs/dsmr.js @@ -0,0 +1,1357 @@ +/** + *__ _____ ____ ___ ___ ___ + *\ \ / / _ \| __ )_ _|_ _/ _ \ + * \ V / | | | _ \| | | | | | | + * | || |_| | |_) | | | | |_| | + * |_| \___/|____/___|___\__\_\ + * + * + * @brief This YOBIIQ JS payload decoder/encoder follows the LoRa Alliance Payload Codec API specs (TS013-1.0.0). + * + * @compatibility TTN, TTI, LORIOT, ThingPark, ChirpStack v3/v4 and any LNS that follows LoRa Alliance API specs. + * + * @author Fostin Kpodar + * @version 1.0.0 + * @copyright YOBIIQ B.V. | https://www.yobiiq.com + * + * @release 2025-08-20 + * @update 2025-02-04 + * + * @product P1002001 iQ DSMR (iQ DSMR Basic) + * + * @firmware DSMR firmware version >= 1.0 + * + */ + +// Version Control +var VERSION_CONTROL = { + CODEC : {VERSION: "1.0.0", NAME: "codecVersion"}, + DEVICE: {MODEL : "DSMR", NAME: "genericModel"}, + PRODUCT: {CODE : "P1002005", NAME: "productCode"}, + MANUFACTURER: {COMPANY : "YOBIIQ B.V.", NAME: "manufacturer"}, +}; + +var UPLINK = { + // generic data + GENERIC_DATA : { + CHANNEL : 255, // 0xFF + FPORT_MIN : 50, + FPORT_MAX : 99, + }, + // device data + DEVICE_DATA : { + CHANNEL : 1, // 0x01 + FPORT_MIN : 1, + FPORT_MAX : 10, + }, + // alarm data + ALARM_DATA : { + CHANNEL_BASE : 160, // 0xA0 + CHANNEL : 170, // 0xAA + FPORT : 11, + }, + // threshold historic data + HISTORIC_DATA : { + CHANNEL : 1, // 0x01 + FPORT : 20, + }, + // parameter data + PARAMETER_DATA : { + CHANNEL : 255, // 0xFF + FPORT : 100, + }, + // general + MAC : { + FPORT : 0, + MSG: "MAC COMMAND RECEIVED", + }, + OPTIONAL_KEYS : { // in DEVICE_GENERIC_REGISTERS or in DEVICE_SPECIFIC_REGISTERS + RESOLUTION: "RESOLUTION", + VALUES: "VALUES", + SIGNED: "SIGNED", + DIGIT: "DIGIT", + UNIT: "UNIT", + HEX: "HEX", + }, + COMMON_REGISTERS: { + "0xFE" : {SIZE: 4, NAME : "timestamp"}, + "0x01" : {SIZE: 4, NAME : "dataloggerTimestamp"}, + }, + DOWNLINK : { + SUCCESS : "DOWNLINK COMMAND SUCCEEDED", + FAILURE : "DOWNLINK COMMAND FAILED" + }, + ERRORS : { + CHANNEL: "Unknown channel ", + TYPE: "Unknown type ", + FPORT_INCORRECT: "Incorrect fPort", + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info", +}; + +var DEVICE_GENERIC_REGISTERS = { + "0x64" : {SIZE : 1, NAME : "deviceStatus", + VALUES : { "0x00" : "FACTORY MODE", "0x01" : "NORMAL MODE",}, + }, + "0x65" : {SIZE : 0, NAME : "manufacturer"}, // size to be determinated + "0x66" : {SIZE : 0, NAME : "originalEquipmentManufacturer"}, // size to be determinated + "0x67" : {SIZE : 0, NAME : "deviceModel"}, // size to be determinated + "0x68" : {SIZE : 4, NAME : "deviceSerialNumber"}, + "0x69" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false}, + "0x6A" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false}, + "0x6B" : {SIZE : 1, NAME : "externalPowerStatus", + VALUES : { "0x00" : "AC POWER OFF", "0x01" : "AC POWER ON",}, + }, + "0x6C" : {SIZE : 1, NAME : "batteryPercentage"}, + "0x6D" : {SIZE : 2, NAME : "batteryVoltage", RESOLUTION: 0.001}, + "0x78" : {SIZE: 1, NAME : "internalCircuitTemperatureAlarm", + VALUES: {"0x00" : "NORMAL", "0x01" : "ALARM",} + }, + "0x79" : {SIZE: 4, NAME : "internalCircuitTemperatureNumberOfAlarms",}, + "0x7A" : {SIZE: 2, NAME : "internalCircuitTemperature", RESOLUTION: 0.01, SIGNED: true}, + "0x7B" : {SIZE: 1, NAME : "internalCircuitHumidity",}, + "0x82" : {SIZE: 2, NAME : "ambientTemperature", RESOLUTION: 0.01, SIGNED: true}, + "0x83" : {SIZE: 1, NAME : "ambientHumidity",}, + "0x96" : {SIZE : 1, NAME : "joinStatus", + VALUES : { "0x00" : "OFFLINE", "0x01" : "ONLINE",}, + }, + "0x9D" : {SIZE: 1, NAME : "applicationPort",}, + "0x9E" : {SIZE: 1, NAME : "joinType", + VALUES : { "0x01" : "OTAA",}, + }, + "0x9F" : {SIZE : 1, NAME : "deviceClass", + VALUES : { "0x00" : "CLASS A", "0x01" : "CLASS B", "0x02" : "CLASS C",}, + }, + "0xA0" : {SIZE: 1, NAME: "adr", + VALUES: {"0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + "0xA1" : {SIZE: 1, NAME: "sf", + VALUES: { "0x00" : "SF12BW125", "0x01" : "SF11BW125", "0x02" : "SF10BW125", + "0x03" : "SF9BW125", "0x04" : "SF8BW125", "0x05" : "SF7BW125", "0x06" : "SF7BW250",} + }, + "0xA3" : {SIZE: 1, NAME: "radioMode", SIZE: 1, + VALUES: { "0x00" : "LoRaWAN", "0x01" : "iQ D2D", "0x02" : "LoRaWAN & iQ D2D",} + }, + "0xA4" : {SIZE: 1, NAME: "numberOfJoinAttempts"}, + "0xA5" : {SIZE: 2, NAME: "linkCheckTimeframe",}, + "0xA6" : {SIZE: 1, NAME: "dataRetransmission", + VALUES: { "0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + "0xA7" : {SIZE: 1, NAME: "lorawanWatchdogAlarm", + VALUES: { "0x00" : "NORMAL", "0x01" : "ALARM",} + }, +}; + +var DEVICE_SPECIFIC_REGISTERS = { + "0xB5" : {SIZE: 1, NAME: "serialWatchdogFunction", + VALUES: { "0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + "0xB6" : {SIZE: 2, NAME: "serialWatchdogTimeout",}, + "0xB7" : {SIZE: 1, NAME: "serialWatchdogAlarm", + VALUES: { "0x00" : "NORMAL", "0x01" : "ALARM",} + }, + "0xB0" : {SIZE: 1, NAME: "serialStopBits", + VALUES: { "0x00" : "STOP_BITS_1", "0x01" : "STOP_BITS_1_5", + "0x02": "STOP_BITS_2", "0x03": "STOP_BITS_2_5", + "0x04": "STOP_BITS_3", "0x05": "STOP_BITS_3_5", + "0x06": "STOP_BITS_4" + } + }, + "0xB1" : {SIZE: 1, NAME: "serialDataWidth",}, + "0xB2" : {SIZE: 1, NAME: "serialParity", + VALUES: { "0x00" : "NONE", "0x01" : "ODD", "0x02" : "EVEN",} + }, + "0xB3" : {SIZE: 4, NAME: "serialBaudRate"}, + "0xB4" : {SIZE: 1, NAME: "dsmrProfile", + VALUES: { "0x00" : "NL", "0x01" : "LUX",} + }, + "0xD1" : {SIZE : 4, NAME : "pulseCounterDryInput1",}, + "0xD2" : {SIZE : 4, NAME : "pulseCounterDryInput2",}, + "0xDD" : {SIZE : 16, NAME : "decryptionKey", HEX:true}, + "0xDE" : {SIZE : 1, NAME : "decryptionFunction", + VALUES: { "0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + + "0x10" : {SIZE : 2, NAME : "p1Version", DIGIT: false}, + "0x11" : {SIZE : 4, NAME : "telegramTimestamp",}, + "0x12" : {SIZE : 0, NAME : "equipmentIdentifier",}, + "0x13" : {SIZE : 4, NAME : "electricityDeliveredToClient", UNIT : "Wh", ALIAS: "totalImportedActiveEnergy"}, + "0x14" : {SIZE : 4, NAME : "electricityDeliveredToClientT1", UNIT : "Wh",}, + "0x15" : {SIZE : 4, NAME : "electricityDeliveredToClientT2", UNIT : "Wh",}, + "0x16" : {SIZE : 4, NAME : "electricityDeliveredByClient", UNIT : "Wh", ALIAS: "totalExportedActiveEnergy"}, + "0x17" : {SIZE : 4, NAME : "electricityDeliveredByClientT1", UNIT : "Wh",}, + "0x18" : {SIZE : 4, NAME : "electricityDeliveredByClientT2", UNIT : "Wh",}, + "0x19" : {SIZE : 2, NAME : "tariffIndicator",}, + "0x1A" : {SIZE : 4, NAME : "electricityPowerDelivered", UNIT : "W", SIGNED : true, ALIAS: "totalImportedActivePower"}, + "0x1B" : {SIZE : 4, NAME : "electricityPowerReceived", UNIT : "W", SIGNED : true, ALIAS: "totalExportedActivePower"}, + "0x1C" : {SIZE : 4, NAME : "numberOfPowerFailures",}, + "0x1D" : {SIZE : 4, NAME : "numberOfLongPowerFailures",}, + "0x1E" : {SIZE : 0, NAME : "powerFailureEventLog", SINGLE_LOG_SIZE:8, LOG:true}, + "0x1F" : {SIZE : 4, NAME : "numberOfVoltageSagsL1",}, + "0x20" : {SIZE : 4, NAME : "numberOfVoltageSagsL2",}, + "0x21" : {SIZE : 4, NAME : "numberOfVoltageSagsL3",}, + "0x22" : {SIZE : 4, NAME : "numberOfVoltageSwellsL1",}, + "0x23" : {SIZE : 4, NAME : "numberOfVoltageSwellsL2",}, + "0x24" : {SIZE : 4, NAME : "numberOfVoltageSwellsL3",}, + "0x25" : {SIZE : 4, NAME : "voltageL1", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x26" : {SIZE : 4, NAME : "voltageL2", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x27" : {SIZE : 4, NAME : "voltageL3", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x28" : {SIZE : 4, NAME : "currentL1", UNIT : "A", SIGNED : true,}, + "0x29" : {SIZE : 4, NAME : "currentL2", UNIT : "A", SIGNED : true,}, + "0x2A" : {SIZE : 4, NAME : "currentL3", UNIT : "A", SIGNED : true,}, + "0x2B" : {SIZE : 4, NAME : "activePowerDeliveredL1", UNIT : "W", SIGNED : true, ALIAS: "importedActivePowerL1"}, + "0x2C" : {SIZE : 4, NAME : "activePowerDeliveredL2", UNIT : "W", SIGNED : true, ALIAS: "importedActivePowerL2"}, + "0x2D" : {SIZE : 4, NAME : "activePowerDeliveredL3", UNIT : "W", SIGNED : true, ALIAS: "importedActivePowerL3"}, + "0x2E" : {SIZE : 4, NAME : "activePowerReceivedL1", UNIT : "W", SIGNED : true, ALIAS: "exportedActivePowerL1"}, + "0x2F" : {SIZE : 4, NAME : "activePowerReceivedL2", UNIT : "W", SIGNED : true, ALIAS: "exportedActivePowerL2"}, + "0x30" : {SIZE : 4, NAME : "activePowerReceivedL3", UNIT : "W", SIGNED : true, ALIAS: "exportedActivePowerL3"}, + "0x31" : {SIZE : 2, NAME : "deviceTypeOnChannel1",}, + "0x32" : {SIZE : 0, NAME : "equipmentIdentifierChannel1",}, + "0x33" : {SIZE : 8, NAME : "lastReadingOnChannel1",}, + "0x34" : {SIZE : 2, NAME : "deviceTypeOnChannel2",}, + "0x35" : {SIZE : 0, NAME : "equipmentIdentifierChannel2",}, + "0x36" : {SIZE : 8, NAME : "lastReadingOnChannel2",}, + "0x37" : {SIZE : 2, NAME : "deviceTypeOnChannel3",}, + "0x38" : {SIZE : 0, NAME : "equipmentIdentifierChannel3",}, + "0x39" : {SIZE : 8, NAME : "lastReadingOnChannel3",}, + "0x3A" : {SIZE : 2, NAME : "deviceTypeOnChannel4",}, + "0x3B" : {SIZE : 0, NAME : "equipmentIdentifierChannel4",}, + "0x3C" : {SIZE : 8, NAME : "lastReadingOnChannel4",}, + "0x3D" : {SIZE : 4, NAME : "totalImportedReactiveEnergy", UNIT : "Wh",}, + "0x3E" : {SIZE : 4, NAME : "totalExportedReactiveEnergy", UNIT : "Wh",}, + "0x3F" : {SIZE : 4, NAME : "totalImportedReactivePower", UNIT : "VAR",}, + "0x40" : {SIZE : 4, NAME : "totalExportedReactivePower", UNIT : "VAR",}, + "0x41" : {SIZE : 4, NAME : "activeThreshold", UNIT: "kVA", RESOLUTION : 0.1}, + "0x42" : {SIZE : 4, NAME : "maxImportedExportedCurrent", UNIT : "A", SIGNED : true,}, + "0x43" : {SIZE : 4, NAME : "totalImportedApparentPower", UNIT : "VA",}, + "0x44" : {SIZE : 4, NAME : "totalExportedApparentPower", UNIT : "VA",}, + "0x45" : {SIZE : 1, NAME : "breakerControlState",}, + "0x46" : {SIZE : 1, NAME : "relay1ControlState",}, + "0x47" : {SIZE : 1, NAME : "relay2ControlState",}, + "0x48": {SIZE : 4, NAME : "importedReactivePowerL1", UNIT : "VAR"}, + "0x49": {SIZE : 4, NAME : "importedReactivePowerL2", UNIT : "VAR"}, + "0x4A": {SIZE : 4, NAME : "importedReactivePowerL3", UNIT : "VAR"}, + "0x4B": {SIZE : 4, NAME : "exportedReactivePowerL1", UNIT : "VAR"}, + "0x4C": {SIZE : 4, NAME : "exportedReactivePowerL2", UNIT : "VAR"}, + "0x4D": {SIZE : 4, NAME : "exportedReactivePowerL3", UNIT : "VAR"}, + "0x4E": {SIZE : 1, NAME : "valvePositionGasChannel1",}, + "0x4F": {SIZE : 1, NAME : "valvePositionGasChannel2",}, + "0x50": {SIZE : 1, NAME : "valvePositionGasChannel3",}, + "0x51": {SIZE : 1, NAME : "valvePositionGasChannel4",}, +}; + + +function decodeGenericData(bytes) +{ + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + while(index < bytes.length) + { + var reg = {}; + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != UPLINK.GENERIC_DATA.CHANNEL){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of generic register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_GENERIC_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = DEVICE_GENERIC_REGISTERS[type]; + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + decoded[reg.NAME] = reg.DATA; + if(reg.ALIAS){ + decoded[reg.ALIAS] = reg.DATA; + } + index = index + reg.DATA_SIZE; + } + return decoded; +} + +function decodeDeviceData(bytes) +{ + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var reg = {}; + while(index < bytes.length) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != UPLINK.DEVICE_DATA.CHANNEL){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_SPECIFIC_REGISTERS)){ + if(!(type in DEVICE_GENERIC_REGISTERS)){ + if(!(type in UPLINK.COMMON_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = UPLINK.COMMON_REGISTERS[type]; + }else{ + reg = DEVICE_GENERIC_REGISTERS[type]; + } + }else{ + reg = DEVICE_SPECIFIC_REGISTERS[type]; + } + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + decoded[reg.NAME] = reg.DATA; + if(reg.ALIAS){ + decoded[reg.ALIAS] = reg.DATA; + } + index = index + reg.DATA_SIZE; + } + return decoded; +} + +function decodeAlarmData(bytes) +{ + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var reg = {}; + while(index < bytes.length) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if((channel & UPLINK.ALARM_DATA.CHANNEL_BASE) != UPLINK.ALARM_DATA.CHANNEL_BASE){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_SPECIFIC_REGISTERS)){ + if(!(type in DEVICE_GENERIC_REGISTERS)){ + if(!(type in UPLINK.COMMON_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = UPLINK.COMMON_REGISTERS[type]; + }else{ + reg = DEVICE_GENERIC_REGISTERS[type]; + } + }else{ + reg = DEVICE_SPECIFIC_REGISTERS[type]; + } + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + decoded[reg.NAME] = reg.DATA; + if(reg.ALIAS){ + decoded[reg.ALIAS] = reg.DATA; + } + index = index + reg.DATA_SIZE; + } + return decoded; +} + +function decodeHistoricData(bytes) +{ + var decoded = {}; + var channel = 0; + var type = ""; + var reg = {}; + // package timestamp + var index = 2; // skip first channel and type + var packageTimestamp = getValueFromBytesBigEndianFormat(bytes, index, 4); + var listOfMeasurements = []; + index = index + 4; + while(index < bytes.length) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if((channel & UPLINK.ALARM_DATA.CHANNEL_BASE) != UPLINK.ALARM_DATA.CHANNEL_BASE){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_SPECIFIC_REGISTERS)){ + if(!(type in DEVICE_GENERIC_REGISTERS)){ + if(!(type in UPLINK.COMMON_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = UPLINK.COMMON_REGISTERS[type]; + }else{ + reg = DEVICE_GENERIC_REGISTERS[type]; + } + }else{ + reg = DEVICE_SPECIFIC_REGISTERS[type]; + } + var logItem = {}; + var timestampDelta = getValueFromBytesBigEndianFormat(bytes, index, 2); + index = index + 2; + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + logItem.name = reg.NAME; + logItem.data = reg.DATA; + if(reg.ALIAS){ + logItem.alias = reg.ALIAS; + } + logItem.ts = packageTimestamp - timestampDelta; + index = index + reg.DATA_SIZE; + listOfMeasurements.push(logItem); + } + decoded.packageTimestamp = packageTimestamp; + decoded.listOfMeasurements = listOfMeasurements; + return decoded; +} + +function decodeParameterData(bytes) +{ + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var reg = {}; + while(index < bytes.length) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != UPLINK.PARAMETER_DATA.CHANNEL){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_SPECIFIC_REGISTERS)){ + if(!(type in DEVICE_GENERIC_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = DEVICE_GENERIC_REGISTERS[type]; + }else{ + reg = DEVICE_SPECIFIC_REGISTERS[type]; + } + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + decoded[reg.NAME] = reg.DATA; + if(reg.ALIAS){ + decoded[reg.ALIAS] = reg.DATA; + } + index = index + reg.DATA_SIZE; + } + return decoded; +} + +/** Helper functions **/ + +function decodeRegister(bytes, reg) +{ + var data = 0; + reg.DATA_SIZE = reg.SIZE; + if(UPLINK.OPTIONAL_KEYS.DIGIT in reg) + { + if(reg.DIGIT == false){ + // Decode into "V" + DIGIT STRING + "." DIGIT STRING format + data = getDigitStringArrayNoFormat(bytes, reg.INDEX, reg.DATA_SIZE); + data = "V" + data[0] + "." + data[1]; + }else{ + // Decode into DIGIT STRING format + data = getDigitStringArrayEvenFormat(bytes, reg.INDEX, reg.DATA_SIZE); + data = data.toString().toUpperCase(); + } + reg.DATA = data; + return reg; + } + if(UPLINK.OPTIONAL_KEYS.HEX in reg) + { + data = getDigitStringArrayEvenFormat(bytes, reg.INDEX, reg.DATA_SIZE); + data = data.join('').toUpperCase(); + reg.DATA = data; + return reg; + } + if(reg.VALUES){ + // Decode into HEX byte (VALUES specified in reg.VALUES) + data = "0x" + byteToEvenHEX(bytes[reg.INDEX]); + data = reg.VALUES[data]; + reg.DATA = data; + return reg; + } + if(reg.LOG){ + // power event logs (byte order little-endian) + var index = reg.INDEX; + var decoded = []; + var len = getValueFromBytesLittleEndianFormat(bytes, index, 4); + index = index + 4; + reg.DATA_SIZE = reg.DATA_SIZE + 4; + for(var i=0; i bytes.length) + { + reg.DATA = decoded; + return reg; + } + var log = {}; + log.timestamp = getValueFromBytesLittleEndianFormat(bytes, index, 4); + log.duration = getValueFromBytesLittleEndianFormat(bytes, index+4, 4); + decoded.push(log); + reg.DATA_SIZE = reg.DATA_SIZE + 8; + index = index + 8; + } + reg.DATA = decoded; + return reg; + } + if(reg.DATA_SIZE == 8) + { + // Slave last reading decoding + var decoded = {}; + decoded.timestamp = getValueFromBytesBigEndianFormat(bytes, reg.INDEX, 4); + decoded.value = getValueFromBytesBigEndianFormat(bytes, reg.INDEX+4, 4); + reg.DATA = decoded; + return reg; + } + if(reg.DATA_SIZE == 0) + { + reg.DATA_SIZE = getSizeBasedOnChannel(bytes, reg.INDEX, reg.CHANNEL); + // Decode into STRING format + data = getStringFromBytesBigEndianFormat(bytes, reg.INDEX, reg.DATA_SIZE); + reg.DATA = data; + return reg; + } + + if(reg.NAME == "maxImportedExportedCurrent") + { + // Decode into 2xINT16 format + var decoded = {}; + var val = getValueFromBytesBigEndianFormat(bytes, reg.INDEX, 2); + val = getSignedIntegerFromInteger(val, 2); + decoded.maxImportedCurrent = val; + val = getValueFromBytesBigEndianFormat(bytes, reg.INDEX+2, 2); + val = getSignedIntegerFromInteger(val, 2); + decoded.maxExportedCurrent = val; + reg.DATA = decoded; + return reg; + } + // Decode into DECIMAL format + data = getValueFromBytesBigEndianFormat(bytes, reg.INDEX, reg.DATA_SIZE); + if(reg.SIGNED){ + data = getSignedIntegerFromInteger(data, reg.DATA_SIZE); + } + if(reg.RESOLUTION){ + data = data * reg.RESOLUTION; + data = parseFloat(data.toFixed(2)); + } + reg.DATA = data; + return reg; +} + +function getStringFromBytesBigEndianFormat(bytes, index, size) +{ + var value = ""; + for(var i=0; i=0; i=i-1) + { + value = value + String.fromCharCode(bytes[index+i]); + } + return value; +} + +function getValueFromBytesBigEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=0; i<(size-1); i=i+1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index+size-1]; + return (value >>> 0); // to unsigned +} + +function getValueFromBytesLittleEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=(size-1); i>0; i=i-1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index]; + return (value >>> 0); // to unsigned +} + +function getDigitStringArrayNoFormat(bytes, index, size) +{ + var hexString = [] + for(var i=0; i= UPLINK.GENERIC_DATA.FPORT_MIN && fPort <= UPLINK.GENERIC_DATA.FPORT_MAX){ + decoded = decodeGenericData(bytes); + }else if(fPort >= UPLINK.DEVICE_DATA.FPORT_MIN && fPort <= UPLINK.DEVICE_DATA.FPORT_MAX){ + decoded = decodeDeviceData(bytes); + }else if(fPort == UPLINK.ALARM_DATA.FPORT){ + decoded = decodeAlarmData(bytes); + }else if(fPort == UPLINK.HISTORIC_DATA.FPORT){ + decoded = decodeHistoricData(bytes); + }else if(fPort == UPLINK.PARAMETER_DATA.FPORT){ + decoded = decodeParameterData(bytes); + }else{ + decoded.fPort = fPort; + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.FPORT_INCORRECT; + } + decoded[VERSION_CONTROL.CODEC.NAME] = VERSION_CONTROL.CODEC.VERSION; + decoded[VERSION_CONTROL.DEVICE.NAME] = VERSION_CONTROL.DEVICE.MODEL; + decoded[VERSION_CONTROL.PRODUCT.NAME] = VERSION_CONTROL.PRODUCT.CODE; + decoded[VERSION_CONTROL.MANUFACTURER.NAME] = VERSION_CONTROL.MANUFACTURER.COMPANY; + return decoded; +} + +// Decode uplink function. (ChirpStack v4, TTN, TTI, LORIOT, ThingPark) +// +// Input is an object with the following fields: +// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0] +// - fPort = Uplink fPort. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - data = Object representing the decoded payload. +function decodeUplink(input) { + var errors = []; + var warnings = []; + var decoded = Decode(input.fPort, input.bytes, input.variables); + if(UPLINK.ERROR_NAME in decoded){ + errors.push(decoded[UPLINK.ERROR_NAME]); + } + if(UPLINK.WARNING_NAME in decoded){ + warnings.push(decoded[UPLINK.WARNING_NAME]); + } + return { + data: decoded, + errors: errors, + warnings: warnings + }; +} + +/*************************************************************************************************************/ +// Constants for device downlink +var DEVICE = { + + DOWNLINK : { + TYPE : "Type", + CONFIG : "Config", + PERIODIC: "Periodic", + THRESHOLD: "Threshold", + READING : "Reading" + }, + CONFIG : { + FPORT: 50, + CHANNEL : 255, // 0xFF + REG_MIN_NUMBER : 1, // downlink min number of registers + REG_MAX_NUMBER : 10, // downlink max number of registers + }, + PERIODIC : { + FPORT_MIN: 1, + FPORT_MAX: 5, + CHANNEL : 255, // 0xFF + INTERVAL_TYPE : 20, // 0x14 + MODE_TYPE : 21, // 0x15 + STATUS_TYPE : 22, // 0x16 + REGISTERS_TYPE : 23, // 0x17 + REG_MIN_NUMBER : 1, // downlink min number of registers + REG_MAX_NUMBER : 10, // downlink max number of registers + }, + THRESHOLD : { + FPORT: 11, + CHANNEL : 255, // 0xFF + REGISTER_TYPE : 64, // 0x40 + OPERATION_TYPE : 65, // 0x41 + MIN_TYPE : 66, // 0x42 + MAX_TYPE : 67, // 0x43 + DELTA_TYPE : 68, // 0x44 + LOG_INTERVAL_TYPE : 69, // 0x45 + UPLINK_INTERVAL_TYPE : 70, // 0x46 + UPLINK_MODE_TYPE : 71, // 0x47 + + OPERATION_VALUES : { + "DISABLED": 0, // 0b0000 + "MIN": 1, // 0b0001 + "MAX": 2, // 0b0010 + "DELTA": 4, // 0b0100 + }, + }, + READING: { + FPORT: 100, + CHANNEL : 255, // 0xFF + TYPE : 204, // 0xCC + REG_MIN_NUMBER : 1, // downlink min number of registers + REG_MAX_NUMBER : 10, // downlink max number of registers + }, + + REGISTERS : { + /* device registers */ + // SIZE, MIN and MAX are required if the register is writable (RW permission is "W" or "RW") + // "registerName": {TYPE:
, RW: <"R"/"W"/"RW">, SIZE: , MIN: , MAX: } + // TH indicates that the register is a threshold register (used in threshold downlinks) + + /* generic registers */ + "deviceStatus": {TYPE: 100, /* 0x64 */ RW:"R",}, + "manufacturer": {TYPE: 101, /* 0x65 */ RW:"R",}, + "originalEquipmentManufacturer": {TYPE: 102, /* 0x66 */ RW:"R",}, + "deviceModel": {TYPE: 103, /* 0x67 */ RW:"R",}, + "deviceSerialNumber": {TYPE: 104, /* 0x68 */ RW:"R",}, + "firmwareVersion": {TYPE: 105, /* 0x69 */ RW:"R",}, + "hardwareVersion": {TYPE: 106, /* 0x6A */ RW:"R",}, + "externalPowerStatus": {TYPE: 107, /* 0x6B */ RW:"R",}, + "batteryPercentage": {TYPE: 108, /* 0x6C */ RW:"R",}, + "batteryVoltage": {TYPE: 109, /* 0x6D */ RW:"R",}, + "rebootDevice": {TYPE: 111, /* 0x6F */ SIZE: 1, MIN: 1, MAX: 1, RW:"W",}, + "internalCircuitTemperatureAlarm": {TYPE: 120, /* 0x78 */ RW:"R",}, + "internalCircuitTemperatureNumberOfAlarms": {TYPE: 121, /* 0x79 */ RW:"R",}, + "internalCircuitTemperature": {TYPE: 122, /* 0x7A */ RW:"R",}, + "internalCircuitHumidity": {TYPE: 123, /* 0x7B */ RW:"R",}, + "ambientTemperature": {TYPE: 130, /* 0x82 */ RW:"R",}, + "ambientHumidity": {TYPE: 131, /* 0x83 */ RW:"R",}, + "joinStatus": {TYPE: 150, /* 0x96 */ RW:"R",}, + "applicationPort": {TYPE: 157, /* 0x9D */ SIZE: 1, MIN: 50, MAX: 99, RW:"RW",}, + "joinType": {TYPE: 158, /* 0x9E */ RW:"RW",}, + "deviceClass": {TYPE: 159, /* 0x9F */ RW:"RW",}, + "adr": {TYPE: 160, /* 0xA0 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "sf": {TYPE: 161, /* 0xA1 */ SIZE: 1, MIN: 0, MAX: 6, RW:"RW",}, + "restartLoRaWAN": {TYPE: 162, /* 0xA2 */ SIZE: 1, MIN: 1, MAX: 1, RW:"W",}, + "radioMode": {TYPE: 163, /* 0xA3 */ SIZE: 1, MIN: 0, MAX: 2, RW:"RW",}, + "numberOfJoinAttempts": {TYPE: 164, /* 0xA4 */ SIZE: 1, MIN: 0, MAX: 255, RW:"RW",}, + "linkCheckTimeframe": {TYPE: 165, /* 0xA5 */ SIZE: 2, MIN: 1, MAX: 65535, RW:"RW",}, + "dataRetransmission": {TYPE: 166, /* 0xA6 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "lorawanWatchdogAlarm": {TYPE: 167, /* 0xA7 */ SIZE: 1, MIN: 0, MAX: 1, RW:"R",}, + "serialWatchdogFunction": {TYPE: 181, /* 0xB5 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "serialWatchdogTimeout": {TYPE: 182, /* 0xB6 */ SIZE: 2, MIN: 1, MAX: 65535, RW:"RW",}, + "serialWatchdogAlarm": {TYPE: 183, /* 0xB7 */ SIZE: 1, MIN: 0, MAX: 1, RW:"R",}, + "serialStopBits": {TYPE: 176, /* 0xB0 */ SIZE: 1, MIN: 0, MAX: 6, RW:"RW",}, + "serialDataWidth": {TYPE: 177, /* 0xB1 */ SIZE: 1, MIN: 5, MAX: 9, RW:"RW",}, + "serialParity": {TYPE: 178, /* 0xB2 */ SIZE: 1, MIN: 0, MAX: 2, RW:"RW",}, + "serialBaudRate": {TYPE: 179, /* 0xB3 */ SIZE: 4, MIN: 1200, MAX: 115200, RW:"RW",}, + "dsmrProfile": {TYPE: 180, /* 0xB4 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "decryptionKey": {TYPE: 221, /* 0xDD */ SIZE: 16, RW:"RW", HEX:true,}, + "decryptionFunction": {TYPE: 222, /* 0xDE */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + + + /* specific registers */ + "p1Version": {TYPE: 16, /* 0x10 */ RW:"R",}, + "telegramTimestamp": {TYPE: 17, /* 0x11 */ RW:"R",}, + "equipmentIdentifier": {TYPE: 18, /* 0x12 */ RW:"R",}, + "electricityDeliveredToClient": {TYPE: 19, /* 0x13 */ RW:"R", TH:true,}, + "totalImportedActiveEnergy": {TYPE: 19, /* 0x13 */ RW:"R", TH:true, ALIAS:true,}, + "electricityDeliveredToClientT1": {TYPE: 20, /* 0x14 */ RW:"R", TH:true,}, + "electricityDeliveredToClientT2": {TYPE: 21, /* 0x15 */ RW:"R", TH:true,}, + "electricityDeliveredByClient": {TYPE: 22, /* 0x16 */ RW:"R", TH:true,}, + "totalExportedActiveEnergy": {TYPE: 22, /* 0x16 */ RW:"R", TH:true, ALIAS:true,}, + "electricityDeliveredByClientT1": {TYPE: 23, /* 0x17 */ RW:"R", TH:true,}, + "electricityDeliveredByClientT2": {TYPE: 24, /* 0x18 */ RW:"R", TH:true,}, + "tariffIndicator": {TYPE: 25, /* 0x19 */ RW:"R",}, + "electricityPowerDelivered": {TYPE: 26, /* 0x1A */ RW:"R", TH:true,}, + "totalImportedActivePower": {TYPE: 26, /* 0x1A */ RW:"R", TH:true, ALIAS:true,}, + "electricityPowerReceived": {TYPE: 27, /* 0x1B */ RW:"R", TH:true,}, + "totalExportedActivePower": {TYPE: 27, /* 0x1B */ RW:"R", TH:true, ALIAS:true,}, + "numberOfPowerFailures": {TYPE: 28, /* 0x1C */ RW:"R",}, + "numberOfLongPowerFailures": {TYPE: 29, /* 0x1D */ RW:"R",}, + "powerFailureEventLog": {TYPE: 30, /* 0x1E */ RW:"R",}, + "numberOfVoltageSagsL1": {TYPE: 31, /* 0x1F */ RW:"R",}, + "numberOfVoltageSagsL2": {TYPE: 32, /* 0x20 */ RW:"R",}, + "numberOfVoltageSagsL3": {TYPE: 33, /* 0x21 */ RW:"R",}, + "numberOfVoltageSwellsL1": {TYPE: 34, /* 0x22 */ RW:"R",}, + "numberOfVoltageSwellsL2": {TYPE: 35, /* 0x23 */ RW:"R",}, + "numberOfVoltageSwellsL3": {TYPE: 36, /* 0x24 */ RW:"R",}, + "voltageL1": {TYPE: 37, /* 0x25 */ RW:"R", TH:true,}, + "voltageL2": {TYPE: 38, /* 0x26 */ RW:"R", TH:true,}, + "voltageL3": {TYPE: 39, /* 0x27 */ RW:"R", TH:true,}, + "currentL1": {TYPE: 40, /* 0x28 */ RW:"R", TH:true,}, + "currentL2": {TYPE: 41, /* 0x29 */ RW:"R", TH:true,}, + "currentL3": {TYPE: 42, /* 0x2A */ RW:"R", TH:true,}, + "activePowerDeliveredL1": {TYPE: 43, /* 0x2B */ RW:"R", TH:true,}, + "importedActivePowerL1": {TYPE: 43, /* 0x2B */ RW:"R", TH:true, ALIAS:true,}, + "activePowerDeliveredL2": {TYPE: 44, /* 0x2C */ RW:"R", TH:true,}, + "importedActivePowerL2": {TYPE: 44, /* 0x2C */ RW:"R", TH:true, ALIAS:true,}, + "activePowerDeliveredL3": {TYPE: 45, /* 0x2D */ RW:"R", TH:true,}, + "importedActivePowerL3": {TYPE: 45, /* 0x2D */ RW:"R", TH:true, ALIAS:true,}, + "activePowerReceivedL1": {TYPE: 46, /* 0x2E */ RW:"R", TH:true,}, + "exportedActivePowerL1": {TYPE: 46, /* 0x2E */ RW:"R", TH:true, ALIAS:true,}, + "activePowerReceivedL2": {TYPE: 47, /* 0x2F */ RW:"R", TH:true,}, + "exportedActivePowerL2": {TYPE: 47, /* 0x2F */ RW:"R", TH:true, ALIAS:true,}, + "activePowerReceivedL3": {TYPE: 48, /* 0x30 */ RW:"R", TH:true,}, + "exportedActivePowerL3": {TYPE: 48, /* 0x30 */ RW:"R", TH:true, ALIAS:true,}, + "deviceTypeOnChannel1": {TYPE: 49, /* 0x31 */ RW:"R",}, + "equipmentIdentifierChannel1": {TYPE: 50, /* 0x32 */ RW:"R",}, + "lastReadingOnChannel1": {TYPE: 51, /* 0x33 */ RW:"R",}, + "deviceTypeOnChannel2": {TYPE: 52, /* 0x34 */ RW:"R",}, + "equipmentIdentifierChannel2": {TYPE: 53, /* 0x35 */ RW:"R",}, + "lastReadingOnChannel2": {TYPE: 54, /* 0x36 */ RW:"R",}, + "deviceTypeOnChannel3": {TYPE: 55, /* 0x37 */ RW:"R",}, + "equipmentIdentifierChannel3": {TYPE: 56, /* 0x38 */ RW:"R",}, + "lastReadingOnChannel3": {TYPE: 57, /* 0x39 */ RW:"R",}, + "deviceTypeOnChannel4": {TYPE: 58, /* 0x3A */ RW:"R",}, + "equipmentIdentifierChannel4": {TYPE: 59, /* 0x3B */ RW:"R",}, + "lastReadingOnChannel4": {TYPE: 60, /* 0x3C */ RW:"R",}, + "totalImportedReactiveEnergy": {TYPE: 61, /* 0x3D */ RW:"R",}, + "totalExportedReactiveEnergy": {TYPE: 62, /* 0x3E */ RW:"R",}, + "totalImportedReactivePower": {TYPE: 63, /* 0x3F */ RW:"R",}, + "totalExportedReactivePower": {TYPE: 64, /* 0x40 */ RW:"R",}, + "activeThreshold": {TYPE: 65, /* 0x41 */ RW:"R",}, + "maxImportedExportedCurrent": {TYPE: 66, /* 0x42 */ RW:"R",}, + "totalImportedApparentPower": {TYPE: 67, /* 0x43 */ RW:"R",}, + "totalExportedApparentPower": {TYPE: 68, /* 0x44 */ RW:"R",}, + "breakerControlState": {TYPE: 69, /* 0x45 */ RW:"R",}, + "relay1ControlState": {TYPE: 70, /* 0x46 */ RW:"R",}, + "relay2ControlState": {TYPE: 71, /* 0x47 */ RW:"R",}, + "importedReactivePowerL1": {TYPE: 72, /* 0x48 */ RW:"R",}, + "importedReactivePowerL2": {TYPE: 73, /* 0x49 */ RW:"R",}, + "importedReactivePowerL3": {TYPE: 74, /* 0x4A */ RW:"R",}, + "exportedReactivePowerL1": {TYPE: 75, /* 0x4B */ RW:"R",}, + "exportedReactivePowerL2": {TYPE: 76, /* 0x4C */ RW:"R",}, + "exportedReactivePowerL3": {TYPE: 77, /* 0x4D */ RW:"R",}, + "valvePositionGasChannel1": {TYPE: 78, /* 0x4E */ RW:"R",}, + "valvePositionGasChannel2": {TYPE: 79, /* 0x4F */ RW:"R",}, + "valvePositionGasChannel3": {TYPE: 80, /* 0x50 */ RW:"R",}, + "valvePositionGasChannel4": {TYPE: 81, /* 0x51 */ RW:"R",}, + }, + ERRORS : { + CMD_INVALID: "Invalid command", + CMD_REGISTER_NOT_FOUND: "Register not found in the device registers", + CMD_REGISTER_NOT_THRESHOLD: "Register is not a threshold register", + CMD_REGISTER_NOT_WRITABLE: "Register not writable", + CMD_REGISTER_NOT_READABLE: "Register not readable", + CMD_REGISTER_NUMBER_INVALID: "Invalid number of registers", + CMD_DATA_INVALID: "Invalid data in the command", + CMD_FPORT_INVALID: "Invalid fPort in the command", + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info", +}; + +/************************************************************************************************************/ + +// Encode encodes the given object into an array of bytes. (ChirpStack v3) +// - fPort contains the LoRaWAN fPort number +// - obj is an object, e.g. {"temperature": 22.5} +// - variables contains the device variables e.g. {"calibration": "3.5"} (both the key / value are of type string) +// The function must return an array of bytes, e.g. [225, 230, 255, 0] +function Encode(fPort, obj, variables) { + if(!(DEVICE.DOWNLINK.TYPE in obj)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please add " + DEVICE.DOWNLINK.TYPE + " to the command"; + return []; // error + } + if(obj[DEVICE.DOWNLINK.TYPE] == DEVICE.DOWNLINK.CONFIG){ + if(fPort != DEVICE.CONFIG.FPORT){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_FPORT_INVALID; + return []; // error + } + return encodeDeviceConfiguration(obj[DEVICE.DOWNLINK.CONFIG]); + }else if(obj[DEVICE.DOWNLINK.TYPE] == DEVICE.DOWNLINK.PERIODIC){ + if(fPort < DEVICE.PERIODIC.FPORT_MIN || fPort > DEVICE.PERIODIC.FPORT_MAX){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_FPORT_INVALID; + return []; // error + } + return encodeUplinkConfiguration(obj[DEVICE.DOWNLINK.PERIODIC]); + }else if(obj[DEVICE.DOWNLINK.TYPE] == DEVICE.DOWNLINK.THRESHOLD){ + if(fPort != DEVICE.THRESHOLD.FPORT){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_FPORT_INVALID; + return []; // error + } + return encodeThresholdConfiguration(obj[DEVICE.DOWNLINK.THRESHOLD]); + }else if(obj[DEVICE.DOWNLINK.TYPE] == DEVICE.DOWNLINK.READING){ + if(fPort != DEVICE.READING.FPORT){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_FPORT_INVALID; + return []; // error + } + return encodeParameterReading(obj[DEVICE.DOWNLINK.READING]); + } + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please check " + obj[DEVICE.DOWNLINK.TYPE] + " in the command"; + return []; // error +} + +// Encode downlink function. (ChirpStack v4 , TTN, TTI, LORIOT, ThingPark) +// +// Input is an object with the following fields: +// - data = Object representing the payload that must be encoded. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - bytes = Byte array containing the downlink payload. +function encodeDownlink(input) { + var fPort = DEVICE.CONFIG.FPORT; // by default use config fPort (50) + if(input.data.fPort) + { + fPort = input.data.fPort; + } + var errors = []; + var warnings = []; + var encoded = Encode(fPort, input.data, input.variables); + if(DEVICE.ERROR_NAME in DEVICE) + { + errors.push(DEVICE[DEVICE.ERROR_NAME]); + } + if(DEVICE.WARNING_NAME in DEVICE) + { + warnings.push(DEVICE[DEVICE.WARNING_NAME]); + } + return { + bytes: encoded, + fPort: fPort, + errors: errors, + warnings : warnings + }; +} + + +/************************************************************************************************************/ + + +function encodeDeviceConfiguration(cmdArray) +{ + var encoded = []; + var reg = {}; + var regName = ""; + + if(!(cmdArray)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please add " + DEVICE.DOWNLINK.CONFIG + " array to the command"; + return []; // error + } + if(cmdArray.length < DEVICE.CONFIG.REG_MIN_NUMBER || + cmdArray.length > DEVICE.CONFIG.REG_MAX_NUMBER){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_REGISTER_NUMBER_INVALID + + ": please check " + DEVICE.DOWNLINK.CONFIG + " in the command"; + return []; + } + + for(var i=0; i reg.MAX){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check " + regName + " in the command"; + return []; // error + } + encoded.push(DEVICE.CONFIG.CHANNEL); + encoded.push(reg.TYPE); + if(reg.SIZE == 4) + { + encoded.push((cmdObj.Value >> 24) & 255); + encoded.push((cmdObj.Value >> 16) & 255); + encoded.push((cmdObj.Value >> 8) & 255); + encoded.push(cmdObj.Value & 255); + }else if(reg.SIZE == 2){ + encoded.push((cmdObj.Value >> 8) & 255); + encoded.push(cmdObj.Value & 255); + }else if(reg.SIZE == 1){ + encoded.push(cmdObj.Value); + } + } + return encoded; +} + +function encodeUplinkConfiguration(cmdObj) +{ + var encoded = []; + var reg = {}; + var regName = ""; + + if(!(cmdObj)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please add " + DEVICE.DOWNLINK.PERIODIC + " object to the command"; + return []; // error + } + if(!("UplinkInterval" in cmdObj) || !("Mode" in cmdObj) || + !("Status" in cmdObj) || !("Registers" in cmdObj)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID; + return []; // error + } + // Encode UplinkInterval, Mode, Status + if(cmdObj.UplinkInterval < 0 || cmdObj.UplinkInterval > 65535){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check UplinkInterval in the command"; + return []; // error + } + encoded.push(DEVICE.PERIODIC.CHANNEL); + encoded.push(DEVICE.PERIODIC.INTERVAL_TYPE); + encoded.push((cmdObj.UplinkInterval >> 8) & 255); + encoded.push(cmdObj.UplinkInterval & 255); + + if(cmdObj.Mode < 0 || cmdObj.Mode > 1){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check Mode in the command"; + return []; // error + } + encoded.push(DEVICE.PERIODIC.CHANNEL); + encoded.push(DEVICE.PERIODIC.MODE_TYPE); + encoded.push(cmdObj.Mode); + + if(cmdObj.Status < 0 || cmdObj.Status > 1){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check Status in the command"; + return []; // error + } + encoded.push(DEVICE.PERIODIC.CHANNEL); + encoded.push(DEVICE.PERIODIC.STATUS_TYPE); + encoded.push(cmdObj.Status); + // Encode registers + if(cmdObj.Registers.length < DEVICE.PERIODIC.REG_MIN_NUMBER || + cmdObj.Registers.length > DEVICE.PERIODIC.REG_MAX_NUMBER){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_REGISTER_NUMBER_INVALID + + ": please check Registers in the command"; + return []; // Error + } + encoded.push(DEVICE.PERIODIC.CHANNEL); + encoded.push(DEVICE.PERIODIC.REGISTERS_TYPE); + for(var i=0; i 65535){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check LogInterval in the command"; + return []; // error + } + encoded.push(DEVICE.THRESHOLD.CHANNEL); + encoded.push(DEVICE.THRESHOLD.LOG_INTERVAL_TYPE); + encoded.push((cmdObj.LogInterval >> 8) & 255); + encoded.push(cmdObj.LogInterval & 255); + } + + // Encode Operation, MinThreshold, MaxThreshold, DeltaThreshold + var op = DEVICE.THRESHOLD.OPERATION_VALUES.DISABLED; + var thresholds = [ + { name: "MinThreshold", type: DEVICE.THRESHOLD.MIN_TYPE, + flag: DEVICE.THRESHOLD.OPERATION_VALUES.MIN, opKey: "MIN" }, + { name: "MaxThreshold", type: DEVICE.THRESHOLD.MAX_TYPE, + flag: DEVICE.THRESHOLD.OPERATION_VALUES.MAX, opKey: "MAX" }, + { name: "DeltaThreshold", type: DEVICE.THRESHOLD.DELTA_TYPE, + flag: DEVICE.THRESHOLD.OPERATION_VALUES.DELTA, opKey: "DELTA" } + ]; + if("Operation" in cmdObj){ + if(!Array.isArray(cmdObj.Operation)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check Operation in the command (it must be an array)"; + return []; // error + } + for(var i=0; i> 24) & 255); + encoded.push((value >> 16) & 255); + encoded.push((value >> 8) & 255); + encoded.push(value & 255); + } + if ((op & th.flag) === th.flag) { + if (!(th.name in cmdObj)) { + warnings += "Threshold operation includes " + th.opKey + + ", but " + th.name + " is not present in this command. "; + } + } + } + if(warnings != ""){ + DEVICE[DEVICE.WARNING_NAME] = warnings; + } + + // Encode UplinkInterval if available + if("UplinkInterval" in cmdObj){ + if(cmdObj.UplinkInterval < 0 || cmdObj.UplinkInterval > 65535){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check UplinkInterval in the command"; + return []; // error + } + encoded.push(DEVICE.THRESHOLD.CHANNEL); + encoded.push(DEVICE.THRESHOLD.UPLINK_INTERVAL_TYPE); + encoded.push((cmdObj.UplinkInterval >> 8) & 255); + encoded.push(cmdObj.UplinkInterval & 255); + } + + // Encode UplinkMode if available + if("UplinkMode" in cmdObj){ + if(cmdObj.UplinkMode < 0 || cmdObj.UplinkMode > 1){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check UplinkMode in the command"; + return []; // error + } + encoded.push(DEVICE.THRESHOLD.CHANNEL); + encoded.push(DEVICE.THRESHOLD.UPLINK_MODE_TYPE); + encoded.push(cmdObj.UplinkMode); + } + return encoded; +} + +function encodeParameterReading(cmdArray) +{ + var encoded = []; + var reg = {}; + var regName = ""; + if(!(cmdArray)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please add " + DEVICE.DOWNLINK.READING + " array to the command"; + return []; // error + } + if(cmdArray.length < DEVICE.READING.REG_MIN_NUMBER || + cmdArray.length > DEVICE.READING.REG_MAX_NUMBER){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_REGISTER_NUMBER_INVALID + + ": please check " + DEVICE.DOWNLINK.READING + " in the command"; + return []; // error + } + encoded.push(DEVICE.READING.CHANNEL); + encoded.push(DEVICE.READING.TYPE); + for(var i=0; i + * @version 1.1.0 + * @copyright YOBIIQ B.V. | https://www.yobiiq.com + * + * @release 06/12/2023 + * @update 10/30/2024 + * + * @author Dominic Hakke + * // Changes in header of document, naming conventions changed. + * + * + * @product P1002015 iQ SD-1001 (Smoke Detector) + * + * + */ + +// Version Control +var VERSION_CONTROL = { + CODEC : {VERSION: "1.1.0", NAME: "codecVersion"}, + DEVICE: {MODEL : "SD-1001", NAME: "deviceModel"}, + PRODUCT: {CODE : "1002015", NAME: "productCode"}, + MANUFACTURER: {COMPANY : "YOBIIQ B.V.", NAME: "manufacturer"}, +} + +// Configuration constants for device basic info +var CONFIG_INFO = { + FPORT : 50, + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "0x09" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false}, + "0x0A" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false}, + "0x16" : {SIZE : 5, NAME : "deviceSerialNumber", DIGIT: true}, + "0x0F" : {SIZE : 1, NAME : "deviceClass", + VALUES : { + "0x00" : "Class A", + "0x01" : "Class B", + "0x02" : "Class C", + }, + }, + "0x0B" : {SIZE : 1, NAME : "powerEvent", + VALUES : { + "0x00" : "AC Power Off", + "0x01" : "AC Power On", + }, + }, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +// Configuration constants for data registers + var CONFIG_DATA = { + FPORT : 8, + CHANNELS : { + "0x01" : {SIZE : 1, NAME : "batteryLevelInPercentage",}, + "0x02" : {SIZE : 1, NAME : "powerEvent", + VALUES : { + "0x00" : "AC Power Off", + "0x01" : "AC Power On", + }, + }, + "0x03" : {SIZE : 1, NAME : "lowBatteryAlarm", + VALUES : { + "0x00" : "Normal", + "0x01" : "Alarm", + }, + }, + "0x04" : {SIZE : 1, NAME : "faultAlarm", + VALUES : { + "0x00" : "Normal", + "0x01" : "Alarm", + }, + }, + "0x05" : {SIZE : 1, NAME : "smokeAlarm", + VALUES : { + "0x00" : "Normal", + "0x01" : "Alarm", + }, + }, + "0x06" : {SIZE : 1, NAME : "interconnectAlarm", + VALUES : { + "0x00" : "Normal", + "0x01" : "Alarm", + }, + }, + "0x07" : {SIZE : 1, NAME : "testButtonPressed", + VALUES : { + "0x00" : "Normal", + "0x01" : "Pushed", + }, + }, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +function isBasicInformation(bytes, fPort) +{ + if(fPort == CONFIG_INFO.FPORT) + { + return true; + } + // Example: ff090100 ff0a0102 ff162404152795 ff0f02 ff0b01 + if(bytes[0] == CONFIG_INFO.CHANNEL && + bytes[4] == CONFIG_INFO.CHANNEL && + bytes[8] == CONFIG_INFO.CHANNEL + ) + { + return true + } + return false; +} + +function decodeBasicInformation(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_INFO.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_INFO.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + if(channel != CONFIG_INFO.CHANNEL) + { + continue; // next byte + } + // Type of basic information + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + var info = CONFIG_INFO.TYPES[type] + size = info.SIZE; + // Decoding + var value = 0; + if(size != 0) + { + if("DIGIT" in info) + { + if(info.DIGIT == false) + { + // Decode into "V" + DIGIT STRING + "." DIGIT STRING format + value = getDigitStringArrayNoFormat(bytes, index, size); + value = "V" + value[0] + "." + value[1]; + }else + { + // Decode into DIGIT STRING format + value = getDigitStringArrayEvenFormat(bytes, index, size).join(""); + value = parseInt(value, 10); + } + } + else if("VALUES" in info) + { + // Decode into HEX STRING (VALUES specified in CONFIG_INFO) + value = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + value = info.VALUES[value]; + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + decoded[info.NAME] = value; + index = index + size; + } + } + }catch(error) + { + decoded[CONFIG_INFO.ERROR_NAME] = error.message; + } + + return decoded; +} + +function decodeDeviceData(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = ""; + var type = 0; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_DATA.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_DATA.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + // Channel of device data + channel = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + // Type of device data + type = bytes[index]; + index = index + 1; + + // No type checking + + var config = CONFIG_DATA.CHANNELS[channel] + size = config.SIZE; + // Decoding + var value = 0; + if("VALUES" in config) + { + // Decode into STRING (VALUES specified in CONFIG_DATA) + value = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + value = config.VALUES[value]; + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + decoded[config.NAME] = value; + index = index + size; + } + }catch(error) + { + decoded[CONFIG_DATA.ERROR_NAME] = error.message; + } + return decoded; +} + +function getValueFromBytesBigEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=0; i<(size-1); i=i+1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index+size-1] + return (value >>> 0); // to unsigned +} + +function getValueFromBytesLittleEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=(size-1); i>0; i=i-1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index] + return (value >>> 0); // to unsigned +} + +function getDigitStringArrayNoFormat(bytes, index, size) +{ + var hexString = [] + for(var i=0; i= config["MIN"] && obj[field[1]] <= config["MAX"]) + { + encoded[index] = CONFIG_DEVICE.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + if(config.SIZE == 1) + { + encoded[index] = value; + index = index + 1; + }else if(config.SIZE == 2) + { + switch(config.TYPE) + { + case 3: // reporting interval + var lowByte = value % 256; + encoded[index] = ((lowByte & parseInt("0x0F", 16)) << 4) + (lowByte >> 4); + index = index + 1; + encoded[index] = (value >> 8) % 256; + index = index + 1; + break; + default: + encoded[index] = (value >> 8) % 256; + index = index + 1; + encoded[index] = value % 256; + index = index + 1; + break; + } + } + }else + { + // Error + return []; + } + }catch(error) + { + // Error + return []; + } + return encoded; +} \ No newline at end of file diff --git a/vendors/yobiiq/codecs/test_decode_dsmr.json b/vendors/yobiiq/codecs/test_decode_dsmr.json new file mode 100644 index 0000000..24031f8 --- /dev/null +++ b/vendors/yobiiq/codecs/test_decode_dsmr.json @@ -0,0 +1,166 @@ +[ + { + "description": "fPort 0 - MAC command", + "input": { + "fPort": 0, + "bytes": [0] + }, + "expected": { + "data": { + "mac": "MAC COMMAND RECEIVED", + "fPort": 0, + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 50 - downlink success", + "input": { + "fPort": 50, + "bytes": [0] + }, + "expected": { + "data": { + "info": "DOWNLINK COMMAND SUCCEEDED", + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 50 - generic registers", + "input": { + "fPort": 50, + "bytes": [ + 255, 100, 1, + 255, 109, 12, 228, + 255, 159, 2, + 255, 160, 1 + ] + }, + "expected": { + "data": { + "deviceStatus": "NORMAL MODE", + "batteryVoltage": 3.3, + "deviceClass": "CLASS C", + "adr": "ENABLED", + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 3 - device data with aliases", + "input": { + "fPort": 3, + "bytes": [ + 1, 19, 0, 0, 48, 57, + 1, 26, 0, 0, 4, 76, + 1, 37, 0, 0, 8, 252, + 1, 40, 0, 0, 0, 25 + ] + }, + "expected": { + "data": { + "electricityDeliveredToClient": 12345, + "totalImportedActiveEnergy": 12345, + "electricityPowerDelivered": 1100, + "totalImportedActivePower": 1100, + "voltageL1": 230, + "currentL1": 25, + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 11 - alarm data", + "input": { + "fPort": 11, + "bytes": [ + 160, 122, 9, 41, + 161, 167, 1 + ] + }, + "expected": { + "data": { + "internalCircuitTemperature": 23.45, + "lorawanWatchdogAlarm": "ALARM", + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 20 - historic data with one measurement", + "input": { + "fPort": 20, + "bytes": [ + 1, 254, 0, 0, 3, 232, + 160, 19, 0, 10, 0, 0, 0, 100 + ] + }, + "expected": { + "data": { + "packageTimestamp": 1000, + "listOfMeasurements": [ + { + "name": "electricityDeliveredToClient", + "data": 100, + "alias": "totalImportedActiveEnergy", + "ts": 990 + } + ], + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 100 - parameter data", + "input": { + "fPort": 100, + "bytes": [ + 255, 180, 1, + 255, 221, 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255 + ] + }, + "expected": { + "data": { + "dsmrProfile": "LUX", + "decryptionKey": "00112233445566778899AABBCCDDEEFF", + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + } +] \ No newline at end of file diff --git a/vendors/yobiiq/codecs/test_decode_sd1001.json b/vendors/yobiiq/codecs/test_decode_sd1001.json new file mode 100644 index 0000000..4a20d6e --- /dev/null +++ b/vendors/yobiiq/codecs/test_decode_sd1001.json @@ -0,0 +1,90 @@ +[ + { + "description": "Basic information on fPort 50", + "input": { + "fPort": 50, + "bytes": [ + 255, 9, 1, 0, + 255, 10, 1, 2, + 255, 22, 36, 4, 21, 39, 149, + 255, 15, 2, + 255, 11, 1 + ] + }, + "expected": { + "data": { + "hardwareVersion": "V1.0", + "firmwareVersion": "V1.2", + "deviceSerialNumber": 2404152795, + "deviceClass": "Class C", + "powerEvent": "AC Power On", + "codecVersion": "1.1.0", + "deviceModel": "SD-1001", + "productCode": "1002015", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "Telemetry on fPort 8", + "input": { + "fPort": 8, + "bytes": [ + 1, 0, 95, + 2, 0, 1, + 3, 0, 0, + 4, 0, 0, + 5, 0, 1, + 6, 0, 0, + 7, 0, 1 + ] + }, + "expected": { + "data": { + "batteryLevelInPercentage": 95, + "powerEvent": "AC Power On", + "lowBatteryAlarm": "Normal", + "faultAlarm": "Normal", + "smokeAlarm": "Alarm", + "interconnectAlarm": "Normal", + "testButtonPressed": "Pushed", + "codecVersion": "1.1.0", + "deviceModel": "SD-1001", + "productCode": "1002015", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "Downlink acknowledgement success", + "input": { + "fPort": 8, + "bytes": [0] + }, + "expected": { + "data": { + "info": "Downlink command succeeded", + "codecVersion": "1.1.0", + "deviceModel": "SD-1001", + "productCode": "1002015", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "Downlink acknowledgement failed", + "input": { + "fPort": 8, + "bytes": [1] + }, + "expected": { + "data": { + "warning": "Downlink command failed", + "codecVersion": "1.1.0", + "deviceModel": "SD-1001", + "productCode": "1002015", + "manufacturer": "YOBIIQ B.V." + } + } + } +] \ No newline at end of file diff --git a/vendors/yobiiq/codecs/test_encode_dsmr.json b/vendors/yobiiq/codecs/test_encode_dsmr.json new file mode 100644 index 0000000..15113e4 --- /dev/null +++ b/vendors/yobiiq/codecs/test_encode_dsmr.json @@ -0,0 +1,99 @@ +[ + { + "description": "Config downlink (default fPort 50)", + "input": { + "data": { + "Type": "Config", + "Config": [ + { "Param": "adr", "Value": 1 }, + { "Param": "linkCheckTimeframe", "Value": 300 } + ] + } + }, + "expected": { + "bytes": [255, 160, 1, 255, 165, 1, 44], + "fPort": 50, + "errors": [], + "warnings": [] + } + }, + { + "description": "Config downlink with HEX register", + "input": { + "data": { + "Type": "Config", + "fPort": 50, + "Config": [ + { "Param": "decryptionKey", "Value": "00112233445566778899AABBCCDDEEFF" } + ] + } + }, + "expected": { + "bytes": [255, 221, 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255], + "fPort": 50, + "errors": [], + "warnings": [] + } + }, + { + "description": "Periodic uplink configuration (fPort 2)", + "input": { + "data": { + "Type": "Periodic", + "fPort": 2, + "Periodic": { + "UplinkInterval": 600, + "Mode": 1, + "Status": 1, + "Registers": ["electricityDeliveredToClient", "voltageL1"] + } + } + }, + "expected": { + "bytes": [255, 20, 2, 88, 255, 21, 1, 255, 22, 1, 255, 23, 19, 37], + "fPort": 2, + "errors": [], + "warnings": [] + } + }, + { + "description": "Threshold configuration (fPort 11)", + "input": { + "data": { + "Type": "Threshold", + "fPort": 11, + "Threshold": { + "Register": "electricityPowerDelivered", + "Operation": ["MIN", "MAX"], + "MinThreshold": 1000, + "MaxThreshold": 2000, + "LogInterval": 60, + "UplinkInterval": 120, + "UplinkMode": 1 + } + } + }, + "expected": { + "bytes": [255, 64, 26, 255, 69, 0, 60, 255, 65, 3, 255, 66, 0, 0, 3, 232, 255, 67, 0, 0, 7, 208, 255, 70, 0, 120, 255, 71, 1], + "fPort": 11, + "errors": [], + "warnings": [] + } + }, + { + "description": "Parameter reading request (fPort 100)", + "input": { + "data": { + "Type": "Reading", + "fPort": 100, + "Reading": ["deviceStatus", "electricityDeliveredToClient", "voltageL1"] + } + }, + "expected": { + "bytes": [255, 204, 100, 19, 37], + "fPort": 100, + "errors": [], + "warnings": [] + } + } +] \ No newline at end of file diff --git a/vendors/yobiiq/codecs/test_encode_sd1001.json b/vendors/yobiiq/codecs/test_encode_sd1001.json new file mode 100644 index 0000000..1c013b0 --- /dev/null +++ b/vendors/yobiiq/codecs/test_encode_sd1001.json @@ -0,0 +1,62 @@ +[ + { + "description": "Set reporting interval to 300 seconds", + "input": { + "data": { + "Type": "Config", + "Config": { + "Param": "reportingInterval", + "Value": 300 + } + } + }, + "expected": { + "bytes": [255, 3, 194, 1] + } + }, + { + "description": "Enable smoke detector alarm", + "input": { + "data": { + "Type": "Config", + "Config": { + "Param": "smokeDetector", + "Value": 1 + } + } + }, + "expected": { + "bytes": [255, 0, 1] + } + }, + { + "description": "Silence buzzer for 60 seconds", + "input": { + "data": { + "Type": "Config", + "Config": { + "Param": "silenceBuzzer", + "Value": 60 + } + } + }, + "expected": { + "bytes": [255, 10, 0, 60] + } + }, + { + "description": "Enable confirmed uplink", + "input": { + "data": { + "Type": "Config", + "Config": { + "Param": "confirmedUplink", + "Value": 1 + } + } + }, + "expected": { + "bytes": [255, 1, 1] + } + } +] \ No newline at end of file diff --git a/vendors/yobiiq/devices/yobiiq-dsmr.toml b/vendors/yobiiq/devices/yobiiq-dsmr.toml new file mode 100644 index 0000000..85852e0 --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-dsmr.toml @@ -0,0 +1,13 @@ +[device] +id = "6714f4f8-7ec6-44ae-912c-d9dd6252ae66" +name = "YOBIIQ DSMR" +description = "The YOBIIQ DSMR reads the data output of a Smart Meter utilizing the DSMR protocol." + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classC.toml"] +codec = "dsmr.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/dsmr-smart-metering/" +documentation_url = "https://yobiiq.com/products/dsmr-smart-metering/" diff --git a/vendors/yobiiq/devices/yobiiq-em2101.toml b/vendors/yobiiq/devices/yobiiq-em2101.toml index 49aff7f..917a38c 100644 --- a/vendors/yobiiq/devices/yobiiq-em2101.toml +++ b/vendors/yobiiq/devices/yobiiq-em2101.toml @@ -1,35 +1,13 @@ [device] - # Device name. - name = "YOBIIQ EM2101" - - # Device description. - description = "EM2101 is a 1phase Electricity Meter with built in relay" - - # Device metadata (optional). - [device.metadata] - # Product URL. - product_url = "https://yobiiq.com/products/electricity-meters/iq-em2101-electricity-meter/" - - # Documentation URL. - documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em2101-electricity-meter/" - - - # Device firmware version. - # - # This section can be repeated in case multiple firmware versions exist. - # As a new firmware version can change the supported profiles / regions and - # payload format, each firmware version has its own profiles and codec - # configuration. - [[device.firmware]] - # Firmware version. - version = "1.0.0" - - # List of supported profiles. - # - # This list refers to one or multiple profiles in the profiles/ directory. - profiles = ["EU868-classC.toml"] - - # Payload codec. - # - # In case no codec is available, you can remove this option. - codec = "em2101.js" \ No newline at end of file +id = "09b110be-91d1-420e-8134-b37e630235bb" +name = "YOBIIQ EM2101" +description = "EM2101 is a 1phase Electricity Meter with built in relay" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classC.toml"] +codec = "em2101.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/electricity-meters/iq-em2101-electricity-meter/" +documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em2101-electricity-meter/" diff --git a/vendors/yobiiq/devices/yobiiq-em4301-ct.toml b/vendors/yobiiq/devices/yobiiq-em4301-ct.toml index a770b2e..a96b7c1 100644 --- a/vendors/yobiiq/devices/yobiiq-em4301-ct.toml +++ b/vendors/yobiiq/devices/yobiiq-em4301-ct.toml @@ -1,35 +1,13 @@ [device] - # Device name. - name = "YOBIIQ EM4301-CT" - - # Device description. - description = "EM4301-CT is a 3phase Electricity Meter with measurement through current transformers" - - # Device metadata (optional). - [device.metadata] - # Product URL. - product_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-ct-electricity-meter/" - - # Documentation URL. - documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-ct-electricity-meter/" - - - # Device firmware version. - # - # This section can be repeated in case multiple firmware versions exist. - # As a new firmware version can change the supported profiles / regions and - # payload format, each firmware version has its own profiles and codec - # configuration. - [[device.firmware]] - # Firmware version. - version = "1.0.0" - - # List of supported profiles. - # - # This list refers to one or multiple profiles in the profiles/ directory. - profiles = ["EU868-classC.toml"] - - # Payload codec. - # - # In case no codec is available, you can remove this option. - codec = "em4301.js" \ No newline at end of file +id = "1e66fe6a-93fc-4889-b4e0-6755554225c7" +name = "YOBIIQ EM4301-CT" +description = "EM4301-CT is a 3phase Electricity Meter with measurement through current transformers" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classC.toml"] +codec = "em4301.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-ct-electricity-meter/" +documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-ct-electricity-meter/" diff --git a/vendors/yobiiq/devices/yobiiq-em4301.toml b/vendors/yobiiq/devices/yobiiq-em4301.toml index cf8a895..7ddb7be 100644 --- a/vendors/yobiiq/devices/yobiiq-em4301.toml +++ b/vendors/yobiiq/devices/yobiiq-em4301.toml @@ -1,35 +1,13 @@ [device] - # Device name. - name = "YOBIIQ EM4301" - - # Device description. - description = "EM4301 is a 3phase Electricity Meter for direct connection" - - # Device metadata (optional). - [device.metadata] - # Product URL. - product_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-electricity-meter/" - - # Documentation URL. - documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-electricity-meter/" - - - # Device firmware version. - # - # This section can be repeated in case multiple firmware versions exist. - # As a new firmware version can change the supported profiles / regions and - # payload format, each firmware version has its own profiles and codec - # configuration. - [[device.firmware]] - # Firmware version. - version = "1.0.0" - - # List of supported profiles. - # - # This list refers to one or multiple profiles in the profiles/ directory. - profiles = ["EU868-classC.toml"] - - # Payload codec. - # - # In case no codec is available, you can remove this option. - codec = "em4301.js" \ No newline at end of file +id = "995ec891-4630-4799-a7c9-45091c1afc63" +name = "YOBIIQ EM4301" +description = "EM4301 is a 3phase Electricity Meter for direct connection" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classC.toml"] +codec = "em4301.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-electricity-meter/" +documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-electricity-meter/" diff --git a/vendors/yobiiq/devices/yobiiq-sd1001-ac.toml b/vendors/yobiiq/devices/yobiiq-sd1001-ac.toml new file mode 100644 index 0000000..9a27f0f --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-sd1001-ac.toml @@ -0,0 +1,13 @@ +[device] +id = "a2c24182-cef3-4a07-9877-785eddb9d6d2" +name = "YOBIIQ SD1001-AC" +description = "SD1001 Smoke Detector powered by AC" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classA.toml"] +codec = "sd1001.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/smoke-detector/" +documentation_url = "https://yobiiq.com/products/smoke-detector/" diff --git a/vendors/yobiiq/devices/yobiiq-sd1001-dc.toml b/vendors/yobiiq/devices/yobiiq-sd1001-dc.toml new file mode 100644 index 0000000..504b567 --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-sd1001-dc.toml @@ -0,0 +1,13 @@ +[device] +id = "6c28ed5d-c13e-4a2d-a402-5e4f703761d7" +name = "YOBIIQ SD1001-DC" +description = "SD1001 Smoke Detector powered by DC" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classA.toml"] +codec = "sd1001.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/smoke-detector/" +documentation_url = "https://yobiiq.com/products/smoke-detector/" diff --git a/vendors/yobiiq/profiles/EU868-classA.toml b/vendors/yobiiq/profiles/EU868-classA.toml index 6e1e9f2..5e0a507 100644 --- a/vendors/yobiiq/profiles/EU868-classA.toml +++ b/vendors/yobiiq/profiles/EU868-classA.toml @@ -1,93 +1,25 @@ [profile] - # ID. - # - # Vendor Profile ID (16 bit). - # - # Encoding: 0xRMC0 (one nibble each, lowest nibble reserved=0) - # R = Region : EU868=0x1, US915=0x2, AU915=0x3, AS923=0x4, AS923-2=0x5, - # AS923-3=0x6, AS923-4=0x7, CN470=0x8, IN865=0x9, KR920=0xA, RU864=0xB - # M = MAC ver : 1.0.0=0x1, 1.0.1=0x2, 1.0.2=0x3, 1.0.3=0x4, 1.0.4=0x5, 1.1.0=0x6 - # C = Class : A=0x1, B=0x2, C=0x3 - # - # See also (4.2.4): - # https://resources.lora-alliance.org/technical-recommendations/tr005-lorawan-device-identification-qr-codes - vendor_profile_id = 5392 # EU868 (0x1) + 1.0.4 (0x5) + Class A (0x1) = 0x1510 - - # Region Common-name. - # - # Please refer to the LoRaWAN Regional Parameters specification for the available - # common-names. - region = "EU868" - - # LoRaWAN mac-version (1.x.y). - mac_version = "1.0.4" - - # LoRaWAN Regional Parameters revision. - # - # Examples: A, B, RP002-1.x.y. - reg_params_revision = "RP002-1.0.3" - - # Device supports OTAA. - supports_otaa = true - - # Device supports Class-B. - # - # If set to true, do not forget to configure class_b section below. - supports_class_b = false - - # Device supports Class-C. - # - # If set to true, do not forget to configure class_c section below. - supports_class_c = false - - # Max EIRP supported by device. - max_eirp = 16 - - # ABP settings. - # - # This section must be configured in case supports_otaa is set to false. - [profile.abp] - # RX1 Delay. - rx1_delay = 1 - - # RX1 DR offset. - rx1_dr_offset = 0 - - # RX2 DR. - rx2_dr = 0 - - # RX2 frequency (Hz). - rx2_freq = 869525000 - - - # Class-B settings. - # - # This section must be configured in case supports_class_b is set to true. - [profile.class_b] - # Timeout in seconds. - # - # In case of an confirmed downlink, the device is expected to respond with - # an ack within the given amount of time. - timeout_secs = 120 - - # Ping-slot numbers (k). - # - # The actual amount is 2^k. Valid options are: 0 - 7. - ping_slot_nb_k = 0 - - # Ping-slot DR. - ping_slot_dr = 0 - - # Ping-slot frequency (Hz). - ping_slot_freq = 869525000 - - - # Class-C settings. - # - # This section must be configured in case supports_class_c is set to true. - [profile.class_c] - # Timeout in seconds. - # - # In case of an confirmed downlink, the device is expected to respond with - # an ack within the given amount of time. - timeout_secs = 120 +id = "be14525a-0883-4901-8f4e-6dc85182ebb9" +vendor_profile_id = 5392 +region = "EU868" +mac_version = "1.0.4" +reg_params_revision = "RP002-1.0.3" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/yobiiq/profiles/EU868-classC.toml b/vendors/yobiiq/profiles/EU868-classC.toml index d75ad49..1ae7731 100644 --- a/vendors/yobiiq/profiles/EU868-classC.toml +++ b/vendors/yobiiq/profiles/EU868-classC.toml @@ -1,93 +1,25 @@ [profile] - # ID. - # - # Vendor Profile ID (16 bit). - # - # Encoding: 0xRMC0 (one nibble each, lowest nibble reserved=0) - # R = Region : EU868=0x1, US915=0x2, AU915=0x3, AS923=0x4, AS923-2=0x5, - # AS923-3=0x6, AS923-4=0x7, CN470=0x8, IN865=0x9, KR920=0xA, RU864=0xB - # M = MAC ver : 1.0.0=0x1, 1.0.1=0x2, 1.0.2=0x3, 1.0.3=0x4, 1.0.4=0x5, 1.1.0=0x6 - # C = Class : A=0x1, B=0x2, C=0x3 - # - # See also (4.2.4): - # https://resources.lora-alliance.org/technical-recommendations/tr005-lorawan-device-identification-qr-codes - vendor_profile_id = 5424 - - # Region Common-name. - # - # Please refer to the LoRaWAN Regional Parameters specification for the available - # common-names. - region = "EU868" - - # LoRaWAN mac-version (1.x.y). - mac_version = "1.0.4" - - # LoRaWAN Regional Parameters revision. - # - # Examples: A, B, RP002-1.x.y. - reg_params_revision = "RP002-1.0.3" - - # Device supports OTAA. - supports_otaa = true - - # Device supports Class-B. - # - # If set to true, do not forget to configure class_b section below. - supports_class_b = false - - # Device supports Class-C. - # - # If set to true, do not forget to configure class_c section below. - supports_class_c = true - - # Max EIRP supported by device. - max_eirp = 16 - - # ABP settings. - # - # This section must be configured in case supports_otaa is set to false. - [profile.abp] - # RX1 Delay. - rx1_delay = 1 - - # RX1 DR offset. - rx1_dr_offset = 0 - - # RX2 DR. - rx2_dr = 0 - - # RX2 frequency (Hz). - rx2_freq = 869525000 - - - # Class-B settings. - # - # This section must be configured in case supports_class_b is set to true. - [profile.class_b] - # Timeout in seconds. - # - # In case of an confirmed downlink, the device is expected to respond with - # an ack within the given amount of time. - timeout_secs = 120 - - # Ping-slot numbers (k). - # - # The actual amount is 2^k. Valid options are: 0 - 7. - ping_slot_nb_k = 0 - - # Ping-slot DR. - ping_slot_dr = 0 - - # Ping-slot frequency (Hz). - ping_slot_freq = 869525000 - - - # Class-C settings. - # - # This section must be configured in case supports_class_c is set to true. - [profile.class_c] - # Timeout in seconds. - # - # In case of an confirmed downlink, the device is expected to respond with - # an ack within the given amount of time. - timeout_secs = 120 +id = "0ac9fb62-4a66-4edf-b044-9f0fbee0a7ec" +vendor_profile_id = 5424 +region = "EU868" +mac_version = "1.0.4" +reg_params_revision = "RP002-1.0.3" +supports_otaa = true +supports_class_b = false +supports_class_c = true +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 120 diff --git a/vendors/yobiiq/vendor.toml b/vendors/yobiiq/vendor.toml index 6162c46..9aebbd9 100644 --- a/vendors/yobiiq/vendor.toml +++ b/vendors/yobiiq/vendor.toml @@ -1,24 +1,8 @@ [vendor] - # Vendor name. - name = "YOBIIQ" +id = "dac78711-5cc1-4682-99bc-e8088a555528" +name = "YOBIIQ" +vendor_id = 415 +ouis = ["FC48C9"] - # LoRa Alliance assigned Vendor ID. - # - # See also: - # ttps://resources.lora-alliance.org/document/lora-alliance-vendor-id-20230915 - vendor_id = 415 - - # OUIs owned by vendor (optional). - # - # Example: ouis = ["010203", "40506"] - ouis = ["FC48C9"] - - # List of devices. - # - # This list refers to one or multiple devices in the devices/ directory. - devices = ["example.toml"] - - # Vendor metadata (optional). - [vendor.metadata] - # Vendor homepage. - homepage = "https://www.yobiiq.com" \ No newline at end of file +[vendor.metadata] +homepage = "https://www.yobiiq.com"