diff --git a/vendor/yobiiq/em2101-codec.yaml b/vendor/yobiiq/em2101-codec.yaml new file mode 100644 index 0000000000..4b7a8acf3b --- /dev/null +++ b/vendor/yobiiq/em2101-codec.yaml @@ -0,0 +1,105 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: em2101.js + examples: + - description: Device basic information + input: + fPort: 50 + bytes: [255, 9, 2, 0, 255, 10, 35, 1, 255, 22, 1, 237, 3, 51, 255, 15, 2, 255, 11, 1, 255, 0, 1, 255, 40, 69, 77, 50, 49, 48, 49] + output: + data: + hardwareVersion: 'V2.0' + firmwareVersion: 'V23.1' + deviceSerialNumber: 32310067 + deviceClass: 'Class C' + powerEvent: 'AC Power On' + relayStatus: 'HIGH' + deviceModel: 'EM2101' + codecVersion: '1.0.1' + genericModel: 'EM2101' + productCode: 'P1002009' + manufacturer: 'YOBIIQ B.V.' + + - description: Device default periodic uplink + input: + fPort: 1 + bytes: [1, 1, 103, 60, 41, 36, 2, 3, 103, 60, 37, 220, 3, 4, 0, 0, 0, 0, 4, 5, 0, 0, 233, 208, 11, 10, 0] + output: + data: + timestamp: 1731995940 + dataloggerTimestamp: 1731995100 + activeEnergyImportL1T1: + data: 0 + unit: 'Wh' + activeEnergyImportL1T2: + data: 59856 + unit: 'Wh' + modbusErrorCode: 0 + codecVersion: '1.0.1' + genericModel: 'EM2101' + productCode: 'P1002009' + manufacturer: 'YOBIIQ B.V.' + + - description: Device change of state alarm + input: + fPort: 11 + bytes: [1, 1, 1, 2, 2, 0] + output: + data: + relayStatus: 'CLOSED' + digitalInputStatus: 'OPEN' + codecVersion: '1.0.1' + genericModel: 'EM2101' + productCode: 'P1002009' + manufacturer: 'YOBIIQ B.V.' + + - description: Device logging events + input: + fPort: 60 + bytes: [253, 1, 1, 253, 2, 2, 253, 8, 1] + output: + data: + relaySwitchingOffReason: 'Due to too high current limit' + relayEnableReason: 'By reset from the Lora network' + resetAmountStatus: 'Current reset count is less than the reset amount' + codecVersion: '1.0.1' + genericModel: 'EM2101' + productCode: 'P1002009' + manufacturer: 'YOBIIQ B.V.' + + - description: Device parameters + input: + fPort: 50 + bytes: [255, 60, 0, 100, 255, 61, 0, 250, 255, 62, 14, 16, 255, 63, 0, 60, 255, 64, 0, 60, 255, 65, 0, 60, 255, 66, 0, 60, 255, 67, 2, 88, 255, 68, 3] + output: + data: + currentLimitFallback: + data: 10 + unit: 'A' + voltageLimitFallback: + data: 250 + unit: 'V' + powerLimitFallback: + data: 3600 + unit: 'W' + deactivationDelayFallback: + data: 60 + unit: 's' + activationDelayFallback: + data: 60 + unit: 's' + offsetCurrentFallback: + data: 6 + unit: 'A' + offsetDelayFallback: + data: 60 + unit: 's' + resetTimeFallback: + data: 600 + unit: 's' + resetAmountFallback: 3 + codecVersion: '1.0.1' + genericModel: 'EM2101' + productCode: 'P1002009' + manufacturer: 'YOBIIQ B.V.' diff --git a/vendor/yobiiq/em2101-profile.yaml b/vendor/yobiiq/em2101-profile.yaml new file mode 100644 index 0000000000..40a91a1ead --- /dev/null +++ b/vendor/yobiiq/em2101-profile.yaml @@ -0,0 +1,47 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: '1.0.4' +# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version: +# 1.0: TS001-1.0 +# 1.0.1: TS001-1.0.1 +# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB +# 1.0.3: RP001-1.0.3-RevA +# 1.0.4: RP002-1.0.0 or RP002-1.0.1 +# 1.1: RP001-1.1-RevA or RP001-1.1-RevB +regionalParametersVersion: 'RP002-1.0.3' + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true +# If your device is an ABP device (supportsJoin is false), uncomment the following fields: +# RX1 delay +#rx1Delay: 5 +# RX1 data rate offset +#rx1DataRateOffset: 0 +# RX2 data rate index +#rx2DataRateIndex: 0 +# RX2 frequency (MHz) +#rx2Frequency: 869.525 +# Factory preset frequencies (MHz) +#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] + +# Maximum EIRP +maxEIRP: 16 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: true + +# Whether the end device supports class B +supportsClassB: false +# If your device supports class B, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classBTimeout: 60 +# Ping slot period (seconds) +#pingSlotPeriod: 128 +# Ping slot data rate index +#pingSlotDataRateIndex: 0 +# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping. +#pingSlotFrequency: 869.525 + +# Whether the end device supports class C +supportsClassC: true +# If your device supports class C, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +classCTimeout: 60 diff --git a/vendor/yobiiq/em2101.js b/vendor/yobiiq/em2101.js new file mode 100644 index 0000000000..a62eb6c525 --- /dev/null +++ b/vendor/yobiiq/em2101.js @@ -0,0 +1,978 @@ +// 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=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=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 == 11) + { + // status packet + decoded = {}; + }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) { + 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) + }; +} + + + diff --git a/vendor/yobiiq/iq-dsmr-lrw.png b/vendor/yobiiq/iq-dsmr-lrw.png new file mode 100644 index 0000000000..6315aafe91 Binary files /dev/null and b/vendor/yobiiq/iq-dsmr-lrw.png differ diff --git a/vendor/yobiiq/iq-dsmr-lrw.yaml b/vendor/yobiiq/iq-dsmr-lrw.yaml new file mode 100644 index 0000000000..8533ca7d2d --- /dev/null +++ b/vendor/yobiiq/iq-dsmr-lrw.yaml @@ -0,0 +1,82 @@ +name: iQ DSMR - Dutch Smart Meter Reader +description: The YOBIIQ iQ DSMR (P1) is a member of our Smart Environment Modules family and Smart metering device, DSMR Relay port, a secondary port is available to forward the DSMR signals to third party devices like solar converters or curtailment node. It contains 2 digital inputs for pulse readers from other energy sources and a optional digital output to control based on consumption. + +# Hardware versions (optional, use when you have revisions) +hardwareVersions: + - version: '2.0' + numeric: 2 + +# Firmware versions (at least one is mandatory) +firmwareVersions: + - # Firmware version + version: '2.0' + numeric: 2 + # Corresponding hardware versions (optional) + hardwareVersions: + - '2.0' + + # Firmware features (optional) + # Valid values are: remote rejoin (trigger a join from the application layer), transmission interval (configure how + # often he device sends a message). + features: + - transmission interval + + # LoRaWAN Device Profiles per region + # Supported regions are EU863-870, US902-928, AU915-928, AS923, CN779-787, EU433, CN470-510, KR920-923, IN865-867, + # RU864-870 + profiles: + EU863-870: + id: iq-dsmr-lrw-profile + lorawanCertified: false + codec: iq-dsmr-lrw-codec + + # Sensors that this device features (optional) +# Valid values are: +# 4-20 ma, accelerometer, altitude, analog input, auxiliary, barometer, battery, button, bvoc, co, co2, conductivity, +# current, digital input, dissolved oxygen, distance, dust, energy, gps, gyroscope, h2s, humidity, iaq, level, light, +# lightning, link, magnetometer, moisture, motion, no, no2, o3, particulate matter, ph, pir, pm2.5, pm10, potentiometer, +# power, precipitation, pressure, proximity, pulse count, pulse frequency, radar, rainfall, rssi, smart valve, snr, so2, +# solar radiation, sound, strain, surface temperature, temperature, tilt, time, tvoc, uv, vapor pressure, velocity, +# vibration, voltage, water potential, water, weight, wifi ssid, wind direction, wind speed. +sensors: + - temperature + - humidity + - current + - power + - voltage + - snr + - rssi + - digital input + - pulse count + - button + +# Additional radios that this device has (optional) +# Valid values are: ble, nfc, wifi, cellular. +additionalRadios: + - ble + +# Product and data sheet URLs (optional) +productURL: https://yobiiq.com/products/dsmr-smart-metering/ + +# Photos +photos: + main: iq-dsmr-lrw.png # Image needs to have a transparent background + +# Regulatory compliances (optional) +compliances: + safety: + - body: IEC + norm: EN + standard: 61326-1:2021 + - body: IEC + norm: EN + standard: 61000-6-2:2019 + - body: IEC + norm: EN + standard: 301489-3 V2.1.1 + - body: IEC + norm: EN + standard: 300220-2 V3.1.1 + - body: IEC + norm: EN + standard: 62479:2010 diff --git a/vendor/yobiiq/iq-em4302-ct-lrw.png b/vendor/yobiiq/iq-em4302-ct-lrw.png new file mode 100644 index 0000000000..fe734955e8 Binary files /dev/null and b/vendor/yobiiq/iq-em4302-ct-lrw.png differ diff --git a/vendor/yobiiq/iq-opentherm-lrw-codec.yaml b/vendor/yobiiq/iq-opentherm-lrw-codec.yaml new file mode 100644 index 0000000000..a6e7230eed --- /dev/null +++ b/vendor/yobiiq/iq-opentherm-lrw-codec.yaml @@ -0,0 +1,106 @@ +# Uplink decoder decodes binary data uplink into a JSON object (optional) +# For documentation on writing encoders and decoders, see: https://thethingsstack.io/integrations/payload-formatters/javascript/ +uplinkDecoder: + fileName: iq-opentherm-lrw.js + examples: + - description: Device basic information + input: + fPort: 50 + bytes: [255, 5, 1, 0, 255, 4, 1, 0, 255, 3, 143, 59, 123, 0, 255, 2, 80, 49, 48, 48, 50, 48, 48, 52, 255, 1, 89, 79, 66, 73, 73, 81, 255, 17, 0, 255, 7, 0, 255, 8, 0, 255, 6, 1] + output: + data: + hardwareVersion: 'V1.0' + firmwareVersion: 'V1.0' + deviceSerialNumber: 2403040000 + deviceModel: 'P1002004' + manufacturer: 'YOBIIQ B.V.' + deviceClass: 'Class A' + batteryPercentage: 0 + batteryVoltage: 0 + powerEvent: 'AC Power On' + codecVersion: '1.0.0' + genericModel: 'OTM' + productCode: 'P1002004' + + - description: Device periodic uplink on fPort 1 + input: + fPort: 1 + bytes: [221, 0, 170, 3, 2, 221, 1, 170, 40, 0, 221, 2, 170, 0, 13, 221, 3, 170, 17, 18, 221, 4, 0, 221, 5, 170, 0, 0, 221, 6, 0, 221, 9, 0, 221, 253, 101, 204, 226, 24] + output: + data: + masterStatus: + centralHeating: 'enabled' + domesticHotWater: 'enabled' + cooling: 'disabled' + outsideTemperatureCompensation: 'disabled' + centralHeating2: 'disabled' + slaveStatus: + faultIndication: 'no fault' + centralHeatingMode: 'active' + domesticHotWaterMode: 'inactive' + flame: 'inactive' + coolingMode: 'inactive' + centralHeating2Mode: 'inactive' + diagnosticIndication: 'no diagnostics' + controlSetpointTemperature: 40 + masterMemberIdCode: 13 + slaveConfigurationFlags: + domesticHotWaterPresence: 'present' + controlType: 'modulating' + coolingConfig: 'not supported' + domesticHotWaterConfig: 'instantaneous or not-specified' + pumpControlFunction: 'not allowed' + centralHeating2Presence: 'not present' + slaveMemberIdCode: 18 + remoteCommand: null + applicationSpecificFaultFlags: + serviceRequest: 'not required' + remoteReset: 'disabled' + lowWaterPressure: 'no fault' + gasOrFlame: 'no fault' + airPressure: 'no fault' + waterOverTemperature: 'no fault' + equipmentManufacturerFaultCode: 0 + remoteParameterTransferFlags: null + remoteParameterReadOrWriteFlags: null + remoteOverrideRoomSetpoint: null + dataloggerTimestamp: 1707926040 + codecVersion: '1.0.0' + genericModel: 'OTM' + productCode: 'P1002004' + manufacturer: 'YOBIIQ B.V.' + + - description: Device periodic uplink on fPort 2 + input: + fPort: 2 + bytes: + [221, 16, 0, 221, 17, 0, 221, 18, 170, 13, 0, 221, 115, 170, 18, 10, 221, 24, 171, 23, 43, 221, 25, 170, 41, 0, 221, 26, 170, 35, 128, 221, 27, 0, 221, 28, 0, 221, 253, 101, 204, 226, 84] + output: + data: + roomSetpointTemperature: null + relativeModulationLevel: null + waterPressure: 13 + originalEquipmentManufacturerDiagnosticCode: 4618 + roomTemperature: 23.17 + boilerFlowWaterTemperature: 41 + domesticHotWaterTemperature1: 35.5 + outsideTemperature: null + returnWaterTemperature: null + dataloggerTimestamp: 1707926100 + codecVersion: '1.0.0' + genericModel: 'OTM' + productCode: 'P1002004' + manufacturer: 'YOBIIQ B.V.' + + - description: Device alarms + input: + fPort: 11 + bytes: [170, 254, 101, 204, 229, 14, 170, 0, 2] + output: + data: + timestamp: 1707926798 + configurationAlarm: 'alarm triggered by Outside Temperature' + codecVersion: '1.0.0' + genericModel: 'OTM' + productCode: 'P1002004' + manufacturer: 'YOBIIQ B.V.' diff --git a/vendor/yobiiq/iq-opentherm-lrw-profile.yaml b/vendor/yobiiq/iq-opentherm-lrw-profile.yaml new file mode 100644 index 0000000000..40a91a1ead --- /dev/null +++ b/vendor/yobiiq/iq-opentherm-lrw-profile.yaml @@ -0,0 +1,47 @@ +# LoRaWAN MAC version: 1.0, 1.0.1, 1.0.2, 1.0.3, 1.0.4 or 1.1 +macVersion: '1.0.4' +# LoRaWAN Regional Parameters version. Values depend on the LoRaWAN version: +# 1.0: TS001-1.0 +# 1.0.1: TS001-1.0.1 +# 1.0.2: RP001-1.0.2 or RP001-1.0.2-RevB +# 1.0.3: RP001-1.0.3-RevA +# 1.0.4: RP002-1.0.0 or RP002-1.0.1 +# 1.1: RP001-1.1-RevA or RP001-1.1-RevB +regionalParametersVersion: 'RP002-1.0.3' + +# Whether the end device supports join (OTAA) or not (ABP) +supportsJoin: true +# If your device is an ABP device (supportsJoin is false), uncomment the following fields: +# RX1 delay +#rx1Delay: 5 +# RX1 data rate offset +#rx1DataRateOffset: 0 +# RX2 data rate index +#rx2DataRateIndex: 0 +# RX2 frequency (MHz) +#rx2Frequency: 869.525 +# Factory preset frequencies (MHz) +#factoryPresetFrequencies: [868.1, 868.3, 868.5, 867.1, 867.3, 867.5, 867.7, 867.9] + +# Maximum EIRP +maxEIRP: 16 +# Whether the end device supports 32-bit frame counters +supports32bitFCnt: true + +# Whether the end device supports class B +supportsClassB: false +# If your device supports class B, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +#classBTimeout: 60 +# Ping slot period (seconds) +#pingSlotPeriod: 128 +# Ping slot data rate index +#pingSlotDataRateIndex: 0 +# Ping slot frequency (MHz). Set to 0 if the band supports ping slot frequency hopping. +#pingSlotFrequency: 869.525 + +# Whether the end device supports class C +supportsClassC: true +# If your device supports class C, uncomment the following fields: +# Maximum delay for the end device to answer a MAC request or confirmed downlink frame (seconds) +classCTimeout: 60 diff --git a/vendor/yobiiq/iq-opentherm-lrw.js b/vendor/yobiiq/iq-opentherm-lrw.js new file mode 100644 index 0000000000..6ff4bf8f5f --- /dev/null +++ b/vendor/yobiiq/iq-opentherm-lrw.js @@ -0,0 +1,1289 @@ + +// Version Control +var VERSION_CONTROL = { + CODEC : {VERSION: "1.0.0", NAME: "codecVersion"}, + DEVICE: {MODEL : "OTM", NAME: "genericModel"}, + PRODUCT: {CODE : "P1002004", 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 : { + "0x05" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false}, + "0x04" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false}, + "0x03" : {SIZE : 4, NAME : "deviceSerialNumber"}, + "0x01" : {SIZE : 0, NAME : "manufacturer"}, // size to be determinated + "0x02" : {SIZE : 0, NAME : "deviceModel"}, // size to be determinated + "0x07" : {SIZE : 1, NAME : "batteryPercentage"}, + "0x08" : {SIZE : 1, NAME : "batteryVoltage", RESOLUTION: 0.1}, + "0x11" : {SIZE : 1, NAME : "deviceClass", + VALUES : { + "0x00" : "Class A", + "0x01" : "Class B", + "0x02" : "Class C", + }, + }, + "0x06" : {SIZE : 1, NAME : "powerEvent", + VALUES : { + "0x00" : "AC Power Off", + "0x01" : "AC Power On", + }, + } + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} +var FLAG_MASTER_STATUS = {NAME : "masterStatus", + FLAGS : [ + {NAME : "centralHeating" , false: "disabled", true: "enabled"}, + {NAME : "domesticHotWater" , false: "disabled", true: "enabled"}, + {NAME : "cooling" , false: "disabled", true: "enabled"}, + {NAME : "outsideTemperatureCompensation" , false: "disabled", true: "enabled"}, + {NAME : "centralHeating2" , false: "disabled", true: "enabled"} + ] +} +var FLAG_SLAVE_STATUS = {NAME : "slaveStatus", + FLAGS : [ + {NAME : "faultIndication" , false: "no fault", true: "fault"}, + {NAME : "centralHeatingMode" , false: "inactive", true: "active"}, + {NAME : "domesticHotWaterMode" , false: "inactive", true: "active"}, + {NAME : "flame" , false: "inactive", true: "active"}, + {NAME : "coolingMode" , false: "inactive", true: "active"}, + {NAME : "centralHeating2Mode" , false: "inactive", true: "active"}, + {NAME : "diagnosticIndication" , false: "no diagnostics", true: "diagnostic event"} + ] +} + +var FLAG_SLAVE_CONFIG = {NAME : "slaveConfigurationFlags", + FLAGS : [ + {NAME : "domesticHotWaterPresence" , false: "not present", true: "present"}, + {NAME : "controlType" , false: "modulating", true: "on/off"}, + {NAME : "coolingConfig" , false: "not supported", true: "supported"}, + {NAME : "domesticHotWaterConfig" , false: "instantaneous or not-specified", true: "storage tank"}, + {NAME : "pumpControlFunction" , false: "allowed", true: "not allowed"}, + {NAME : "centralHeating2Presence" , false: "not present", true: "present"} + ] +} + +var FLAG_APP_FAULT = {NAME : "applicationSpecificFaultFlags", + FLAGS : [ + {NAME : "serviceRequest" , false: "not required", true: "required"}, + {NAME : "remoteReset" , false: "disabled", true: "enabled"}, + {NAME : "lowWaterPressure" , false: "no fault", true: "fault"}, + {NAME : "gasOrFlame" , false: "no fault", true: "fault"}, + {NAME : "airPressure" , false: "no fault", true: "fault"}, + {NAME : "waterOverTemperature" , false: "no fault", true: "fault"} + ] +} + +var FLAG_REMOTE_PARAM_XFER = {NAME : "remoteParameterTransferFlags", + FLAGS : [ + {NAME : "domesticHotWaterSetpoint" , false: "disabled", true: "enabled"}, + {NAME : "dentralHeatingSetpointMaximum" , false: "disabled", true: "enabled"} + ] +} + +var FLAG_REMOTE_PARAM_RW = {NAME : "remoteParameterReadOrWriteFlags", + FLAGS : [ + {NAME : "domesticHotWaterSetpoint" , false: "read-only", true: "read/write"}, + {NAME : "centralHeatingSetpointMaximum" , false: "read-only", true: "read/write"} + ] +} + +var FLAG_REMOTE_OVERRIDE_FUNCTION = {NAME : "remoteOverrideFunction", + FLAGS : [ + {NAME : "manualChangePriority" , false: "disabled", true: "enabled"}, + {NAME : "programChangePriority" , false: "disabled", true: "enabled"} + ] +} + +var REMOTE_COMMAND = { + HIGH_BYTE : {NAME: "commandCode", VALUES: {"0x01" : "BoilerReset", "0x02" : "CentralHeatingWaterFilling"}}, + LOW_BYTE : {NAME: "responseCode", CENTER: 128} +} + +// Configuration constants for opentherm data + var CONFIG_OPENTHERM = { + CHANNEL : parseInt("0xDD", 16), + FPORT_MIN : 1, + FPORT_MAX : 5, + DATAPOINT_UNAVAILABLE : parseInt("0x00", 16), + DATAPOINT_AVAILABLE_REFRESHED : parseInt("0xAA", 16), + DATAPOINT_AVAILABLE_NOT_REFRESHED : parseInt("0xAB", 16), + TYPES : { + "0xFE" : {SIZE: 4, TYPE: "U32", NAME : "timestamp"}, + "0xFD" : {SIZE: 4, TYPE: "U32", NAME : "dataloggerTimestamp"}, + "0x00" : {TYPE: "F8F8", FLAG_HB: FLAG_MASTER_STATUS, FLAG_LB: FLAG_SLAVE_STATUS}, + "0x01" : {TYPE: "FLOAT", NAME : "controlSetpointTemperature"}, + "0x02" : {TYPE: "F8U8", FLAG_HB: null, NAME_LB: "masterMemberIdCode"}, + "0x03" : {TYPE: "F8U8", FLAG_HB: FLAG_SLAVE_CONFIG, NAME_LB: "slaveMemberIdCode"}, + "0x04" : {TYPE: "RCMD", NAME : "remoteCommand"}, + "0x05" : {TYPE: "F8U8", FLAG_HB: FLAG_APP_FAULT, NAME_LB: "equipmentManufacturerFaultCode"}, + "0x06" : {TYPE: "F8F8", FLAG_HB: FLAG_REMOTE_PARAM_XFER, FLAG_LB: FLAG_REMOTE_PARAM_RW}, + "0x07" : {TYPE: "FLOAT", NAME: "coolingControlSignalPercentage"}, + "0x08" : {TYPE: "FLOAT", NAME: "controlSetpointTemperature2"}, + "0x09" : {TYPE: "BOOL", NAME: "remoteOverrideRoomSetpoint"}, + "0x0A" : {TYPE: "U8U8", NAME_HB: "numberOfTransparentSlaveParameters", NAME_LB: null}, + "0x0B" : {TYPE: "INDEX", NAME: "transparentSlaveParameter"}, + "0x0C" : {TYPE: "U8U8", NAME_HB: "faultBufferSize", NAME_LB: null}, + "0x0D" : {TYPE: "INDEX", NAME: "faultHistoryBuffer"}, + "0x0E" : {TYPE: "FLOAT", NAME: "maximumRelativeModulationLevel"}, + "0x0F" : {TYPE: "U8U8", NAME_HB: "maximumBoilerCapacity", NAME_LB:"minimumModulationLevel"}, + "0x10" : {TYPE: "FLOAT", NAME: "roomSetpointTemperature"}, + "0x11" : {TYPE: "FLOAT", NAME: "relativeModulationLevel"}, + "0x12" : {TYPE: "FLOAT", NAME: "waterPressure"}, + "0x13" : {TYPE: "FLOAT", NAME: "domesticHotWaterFlowRate"}, + "0x14" : {TYPE: "TIME", NAME: "boilerTime"}, + "0x15" : {TYPE: "DATE", NAME: "calendarDate"}, + "0x16" : {TYPE: "U16", NAME: "calendarYear"}, + "0x17" : {TYPE: "FLOAT", NAME: "roomSetpointTemperature2"}, + "0x18" : {TYPE: "FLOAT", NAME: "roomTemperature"}, + "0x19" : {TYPE: "FLOAT", NAME: "boilerFlowWaterTemperature"}, + "0x1A" : {TYPE: "FLOAT", NAME: "domesticHotWaterTemperature1"}, + "0x1B" : {TYPE: "FLOAT", NAME: "outsideTemperature"}, + "0x1C" : {TYPE: "FLOAT", NAME: "returnWaterTemperature"}, + "0x1D" : {TYPE: "FLOAT", NAME: "solarStorageTemperature"}, + "0x1E" : {TYPE: "FLOAT", NAME: "solarCollectorTemperature"}, + "0x1F" : {TYPE: "FLOAT", NAME: "flowWaterTemperature2"}, + "0x20" : {TYPE: "FLOAT", NAME: "domesticHotWaterTemperature2"}, + "0x21" : {TYPE: "S16", NAME: "boilerExhaustTemperature"}, + "0x30" : {TYPE: "S8S8", NAME: "domesticHotWaterSetpointAdjustmentBounds",}, + "0x31" : {TYPE: "S8S8", NAME: "centralHeatingWaterMaximumSetpointAdjustmentBounds"}, + "0x32" : {TYPE: "S8S8", NAME: "outsideTemperatureCompensationHeatCurveRatioAdjustmentBounds"}, + "0x38" : {TYPE: "FLOAT", NAME: "domesticHotWaterSetpointTemperature"}, + "0x39" : {TYPE: "FLOAT", NAME: "centralHeatingWaterMaximumSetpointTemperature"}, + "0x3A" : {TYPE: "FLOAT", NAME: "outsideTemperatureCompensationHeatCurveRatio"}, + "0x64" : {TYPE: "F8U8", FLAG_HB: FLAG_REMOTE_OVERRIDE_FUNCTION, NAME_LB: null}, + "0x73" : {TYPE: "U16", NAME: "originalEquipmentManufacturerDiagnosticCode"}, + "0x74" : {TYPE: "U16", NAME: "numberOfStartsBurner"}, + "0x75" : {TYPE: "U16", NAME: "numberOfStartsCentralHeatingPump"}, + "0x76" : {TYPE: "U16", NAME: "numberOfStartsDomesticHotWaterPumpOrValve"}, + "0x77" : {TYPE: "U16", NAME: "numberOfStartsBurnerDuringDomesticHotWaterMode"}, + "0x78" : {TYPE: "U16", NAME: "numberOfHoursWhenBurnerOperating"}, + "0x79" : {TYPE: "U16", NAME: "numberOfHoursWhenCentralHeatingPumpOperating"}, + "0x7A" : {TYPE: "U16", NAME: "numberOfHoursWhenDomesticHotWaterPumpOrValveOperating"}, + "0x7B" : {TYPE: "U16", NAME: "numberOfHoursOfBurnerOperatingDuringDomesticHotWaterMode"}, + "0x7C" : {TYPE: "FLOAT", NAME: "masterOpenThermProtocolVersion"}, + "0x7D" : {TYPE: "FLOAT", NAME: "slaveOpenThermProtocolVersion"}, + "0x7E" : {TYPE: "U8U8", NAME_HB: "masterProductType", NAME_LB: "masterProductVersion"}, + "0x7F" : {TYPE: "U8U8", NAME_HB: "slaveProductType", NAME_LB: "slaveProductVersion"}, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + + +// Configuration constants for alarm packet +var CONFIG_ALARM = { + FPORT : 11, + CHANNEL : parseInt("0xAA", 16), + TYPES : { + "0xFE" : {SIZE: 4, NAME : "timestamp"}, + "0x00" : {SIZE: 1, NAME : "configurationAlarm", + VALUES : { + "0x00" : "normal", + "0x01" : "alarm triggered by No Boiler Setpoint Mode but the device is in Controller Mode", + "0x02" : "alarm triggered by Outside Temperature", + "0x03" : "alarm triggered by Dewpoint Offset Temperature ", + "0x04" : "alarm triggered by Dewpoint Temperature ", + "0x05" : "alarm triggered by Delta Room Temperature", + }, + }, + "0x01" : {SIZE: 1, NAME : "boilerWatchdog", + VALUES : { + "0x00" : "normal", + "0x01" : "alarm", + }, + }, + "0x02" : {SIZE : 1, NAME : "thermostatWatchdog", + VALUES : { + "0x00" : "normal", + "0x01" : "alarm", + }, + } + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +// Configuration constants for parameters reading +var CONFIG_PARAMETER = { + FPORT : 100, + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "0x12": {NAME: "adr", SIZE: 1, VALUES: {"0x00" : "disabled", "0x01" : "enabled",}}, + "0x13": {NAME: "sf", SIZE: 1, VALUES: { + "0x00" : "SF12BW125", + "0x01" : "SF11BW125", + "0x02" : "SF10BW125", + "0x03" : "SF9BW125", + "0x04" : "SF8BW125", + "0x05" : "SF7BW125", + "0x06" : "SF7BW250", + } + }, + "0x64": {NAME : "watchdogThermostatTimeout", SIZE: 2, RESOLUTION: 1,}, + "0x65": {NAME : "watchdogBoilerTimeout", SIZE: 2, RESOLUTION: 1,}, + "0x66": {NAME : "deviceOperationMode", SIZE: 1, VALUES: {"0x00" : "normal", "0x01" : "controller",}}, + "0x67": {NAME : "boilerSetpointMode", SIZE: 1, VALUES: { + "0x00" : "n/a", + "0x01" : "MODE1", + "0x02" : "MODE2", + "0x03" : "MODE3", + "0x04" : "MODE4", + } + }, + "0x70": {NAME : "realThermostat", SIZE: 1, VALUES: {"0x00" : "not present", "0x01" : "present",}}, + "0x71": {NAME : "boilerCentralHeating", SIZE: 1, VALUES: {"0x00" : "disabled", "0x01" : "enabled",}}, + "0x72": {NAME : "boilerDomesticHotWater", SIZE: 1, VALUES: {"0x00" : "disabled", "0x01" : "enabled",}}, + "0x73": {NAME : "boilerCooling", SIZE: 1, VALUES: {"0x00" : "disabled", "0x01" : "enabled",}}, + "0x74": {NAME : "boilerOutsideTemperatureCompensation", SIZE: 1, VALUES: {"0x00" : "disabled", "0x01" : "enabled",}}, + "0x75": {NAME : "boilerCentralHeating2", SIZE: 1, VALUES: {"0x00" : "disabled", "0x01" : "enabled",}}, + "0x95": {NAME : "outsideTemperature", SIZE: 2, RESOLUTION: 0.01,}, + "0x96": {NAME : "dewpointOffsetTemperature", SIZE: 2, RESOLUTION: 0.01,}, + "0x97": {NAME : "dewpointTemperature", SIZE: 2, RESOLUTION: 0.01,}, + "0x98": {NAME : "deltaRoomTemperature", SIZE: 2, RESOLUTION: 0.01,}, + "0x99": {NAME : "heatcurveMinimumTemperature", SIZE: 2, RESOLUTION: 0.01,}, + "0x9A": {NAME : "heatcurveMaximumTemperature", SIZE: 2, RESOLUTION: 0.01,}, + "0x9B": {NAME : "outsideTemperature1", SIZE: 2, RESOLUTION: 0.01,}, + "0x9C": {NAME : "supplyTemperature1", SIZE: 2, RESOLUTION: 0.01,}, + "0x9D": {NAME : "outsideTemperature2", SIZE: 2, RESOLUTION: 0.01,}, + "0x9E": {NAME : "supplyTemperature2", SIZE: 2, RESOLUTION: 0.01,}, + "0x9F": {NAME : "compensationFactorHigher", SIZE: 1, RESOLUTION: 1,}, + "0xA0": {NAME : "compensationFactorLower", SIZE: 1, RESOLUTION: 1,}, + "0xA1": {NAME : "compensationValueMinimum", SIZE: 1, RESOLUTION: 1,}, + "0xA2": {NAME : "compensationValueMaximum", SIZE: 1, RESOLUTION: 1,}, + "0xA3": {NAME : "calculatedHeatingCurveValue", SIZE: 2, RESOLUTION: 0.01,}, + "0xA4": {NAME : "calculatedSetpointBoiler", SIZE: 2, RESOLUTION: 0.01,}, + "0xA5": {NAME : "externalHeatDemand", SIZE: 2, RESOLUTION: 0.01,}, + }, + 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; + // Channel checking + if(channel != CONFIG_INFO.CHANNEL) + { + continue; + } + // 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(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().toUpperCase(); + } + + } + 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 + { + if(size == 0) + { + size = getSizeBasedOnChannel(bytes, index, channel); + // Decode into STRING format + value = getStringFromBytesBigEndianFormat(bytes, index, size); + + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + } + if(info.RESOLUTION) + { + value = value * info.RESOLUTION; + value = parseFloat(value.toFixed(2)); + } + + 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 = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_OPENTHERM.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_OPENTHERM.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != CONFIG_OPENTHERM.CHANNEL) + { + continue; + } + // Type of basic information + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + var info = CONFIG_OPENTHERM.TYPES[type] ? CONFIG_OPENTHERM.TYPES[type] : null; + if(info == null) + { + info = {TYPE: "UNKNOWN", NAME : "DataPointID"+parseInt(type)}; + } + size = info.SIZE ? info.SIZE : 0; + if(size == 0) + { + info.avaibility = bytes[index]; + index = index + 1; + size = getSizeBasedOnAvailability(info.avaibility); + }else + { + info.avaibility = CONFIG_OPENTHERM.DATAPOINT_AVAILABLE_REFRESHED; + } + // Decoding + var values = {}; + if(info.avaibility == CONFIG_OPENTHERM.DATAPOINT_UNAVAILABLE && info.NAME) + { + values[info.NAME] = null; + } + switch (info.TYPE) + { + case "U32": + { + // always available + values[info.NAME] = getValueFromBytesBigEndianFormat(bytes, index, size); + break; + } + case "U16": + case "S16": + { + if(info.avaibility != CONFIG_OPENTHERM.DATAPOINT_UNAVAILABLE) + { + values[info.NAME] = getValueFromBytesBigEndianFormat(bytes, index, 2); + if(info.TYPE == "S16") + { + values[info.NAME] = getSignedIntegerFromInteger(values[info.NAME], 2); + } + } + break; + } + case "BOOL": + { + if(info.avaibility != CONFIG_OPENTHERM.DATAPOINT_UNAVAILABLE) + { + values[info.NAME] = getValueFromBytesBigEndianFormat(bytes, index, 2); + if(values[info.NAME] == 0) + { + values[info.NAME] = false; + }else{ + values[info.NAME] = true; + } + } + break; + } + case "FLOAT": + { + if(info.avaibility != CONFIG_OPENTHERM.DATAPOINT_UNAVAILABLE) + { + values[info.NAME] = getValueFLOATFormat(bytes, index); + } + break; + } + case "F8F8": + { + values = getValuesF8F8Format(bytes, index, info); + break; + } + case "F8U8": + { + values = getValuesF8U8Format(bytes, index, info); + break; + } + case "U8U8": + { + values = getValuesU8U8Format(bytes, index, info); + break; + } + case "S8S8": + { + values = getValuesS8S8Format(bytes, index, info); + break; + } + case "TIME": + { + values = getValuesTIMEFormat(bytes, index, info); + break; + } + case "DATE": + { + values = getValuesDATEFormat(bytes, index, info); + break; + } + case "RCMD": + { + values = getValuesRCMDFormat(bytes, index, info); + break; + } + case "INDEX": + { + values = getValuesINDEXFormat(bytes, index, info); + break; + } + case "UNKNOWN": + { + if(info.avaibility != CONFIG_OPENTHERM.DATAPOINT_UNAVAILABLE) + { + values[info.NAME] = "0x" + getDigitStringArrayEvenFormat(bytes, index, size).join("").toUpperCase(); + } + break; + } + default: + break; + } + Object.keys(values).forEach( function(key) { + decoded[key] = values[key]; + }); + index = index + size; + + } + }catch(error) + { + decoded[CONFIG_OPENTHERM.ERROR_NAME] = error.message; + } + + return decoded; +} + +function decodeAlarmPacket(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_ALARM.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_ALARM.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != CONFIG_ALARM.CHANNEL) + { + continue; + } + // Type of alarm + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + var info = CONFIG_ALARM.TYPES[type]; + size = info.SIZE; + // Decoding + var value = 0; + value = getValueFromBytesBigEndianFormat(bytes, index, size); + if(info.VALUES) + { + value = "0x" + toEvenHEX(value.toString(16).toUpperCase()); + value = info.VALUES[value]; + } + decoded[info.NAME] = value; + index = index + size; + } + }catch(error) + { + decoded[CONFIG_ALARM.ERROR_NAME] = error.message; + } + + return decoded; +} + +function decodeParameters(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_PARAMETER.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_PARAMETER.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != CONFIG_PARAMETER.CHANNEL) + { + continue; + } + // Type of parameter + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + var info = CONFIG_PARAMETER.TYPES[type]; + size = info.SIZE; + // Decoding + var value = 0; + value = getValueFromBytesBigEndianFormat(bytes, index, size); + value = getSignedIntegerFromInteger(value, info.SIZE); + if(info.VALUES) + { + value = "0x" + toEvenHEX(value.toString(16).toUpperCase()); + value = info.VALUES[value]; + } + if(info.RESOLUTION) + { + value = value * info.RESOLUTION; + value = parseFloat(value.toFixed(2)); + } + decoded[info.NAME] = value; + index = index + size; + } + }catch(error) + { + decoded[CONFIG_PARAMETER.ERROR_NAME] = error.message; + } + + return decoded; +} + +/** Helper functions **/ + +function getBitValue(byte, indexOfBitInByte) +{ + var bitMask = 0x01 << indexOfBitInByte; + if(byte & bitMask) + { + return true; + } + return false; +} + +function getFlags(byte, info) +{ + var flags = {}; + var flag = {}; + 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_OPENTHERM.FPORT_MIN && fPort <= CONFIG_OPENTHERM.FPORT_MAX) + { + decoded = decodeDeviceData(bytes); + }else if(fPort == CONFIG_ALARM.FPORT) + { + decoded = decodeAlarmPacket(bytes); + }else if(fPort == CONFIG_PARAMETER.FPORT) + { + decoded = decodeParameters(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.OPENTHERM) + { + return encodeOpenthermConfiguration(obj[CONFIG_DOWNLINK.OPENTHERM], variables); + } + else if(obj[CONFIG_DOWNLINK.TYPE] == CONFIG_DOWNLINK.READING) + { + return encodeParamtersReading(obj[CONFIG_DOWNLINK.READING], 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 downlink type +var CONFIG_DOWNLINK = { + TYPE : "Type", + CONFIG : "Config", + OPENTHERM : "Opentherm", + READING : "Reading" +} + +// Constants for device configuration +var CONFIG_DEVICE = { + PORT : 50, + REGISTER_CHANNEL : parseInt("0xFF", 16), + OPENTHERM_CHANNEL : parseInt("0xCC", 16), + READING_TYPE : parseInt("0xCC", 16), + DATA_MAX_SIZE : 9, + REGISTERS : { + "restart": {TYPE: parseInt("0x0B", 16), SIZE: 1, MIN: 1, MAX: 1, RIGHT:"WRITE_ONLY"}, + "adr": {TYPE: parseInt("0x12", 16), SIZE: 1, MIN: 0, MAX: 1,}, + "sf": {TYPE: parseInt("0x13", 16), SIZE: 1, MIN: 0, MAX: 6,}, + "watchdogThermostatTimeout": {TYPE : parseInt("0x64", 16), SIZE: 2, MIN: 1, MAX: 65535,}, + "watchdogBoilerTimeout": {TYPE : parseInt("0x65", 16), SIZE: 2, MIN: 1, MAX: 65535,}, + "deviceOperationMode": {TYPE : parseInt("0x66", 16), SIZE: 1, MIN: 0, MAX: 1,}, + "boilerSetpointMode": {TYPE : parseInt("0x67", 16), SIZE: 1, MIN: 0, MAX: 4,}, + "realThermostat": {TYPE : parseInt("0x70", 16), SIZE: 1, MIN: 0, MAX: 1,}, + "boilerCentralHeating": {TYPE : parseInt("0x71", 16), SIZE: 1, MIN: 0, MAX: 1,}, + "boilerDomesticHotWater": {TYPE : parseInt("0x72", 16), SIZE: 1, MIN: 0, MAX: 1,}, + "boilerCooling": {TYPE : parseInt("0x73", 16), SIZE: 1, MIN: 0, MAX: 1,}, + "boilerOutsideTemperatureCompensation": {TYPE : parseInt("0x74", 16), SIZE: 1, MIN: 0, MAX: 1,}, + "boilerCentralHeating2": {TYPE : parseInt("0x75", 16), SIZE: 1, MIN: 0, MAX: 1,}, + "outsideTemperature": {TYPE : parseInt("0x95", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "dewpointOffsetTemperature": {TYPE : parseInt("0x96", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "dewpointTemperature": {TYPE : parseInt("0x97", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "deltaRoomTemperature": {TYPE : parseInt("0x98", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "heatcurveMinimumTemperature": {TYPE : parseInt("0x99", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "heatcurveMaximumTemperature": {TYPE : parseInt("0x9A", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "outsideTemperature1": {TYPE : parseInt("0x9B", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "supplyTemperature1": {TYPE : parseInt("0x9C", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "outsideTemperature2": {TYPE : parseInt("0x9D", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "supplyTemperature2": {TYPE : parseInt("0x9E", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "compensationFactorHigher": {TYPE : parseInt("0x9F", 16), SIZE: 1, MIN: -128, MAX: 127,}, + "compensationFactorLower": {TYPE : parseInt("0xA0", 16), SIZE: 1, MIN: -128, MAX: 127,}, + "compensationValueMinimum": {TYPE : parseInt("0xA1", 16), SIZE: 1, MIN: -128, MAX: 127,}, + "compensationValueMaximum": {TYPE : parseInt("0xA2", 16), SIZE: 1, MIN: -128, MAX: 127,}, + "calculatedHeatingCurveValue": {TYPE : parseInt("0xA3", 16), SIZE: 2, MIN: -32768, MAX: 32767, RIGHT:"READ_ONLY"}, + "calculatedSetpointBoiler": {TYPE : parseInt("0xA4", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + "externalHeatDemand": {TYPE : parseInt("0xA5", 16), SIZE: 2, MIN: -32768, MAX: 32767,}, + } +} + +function encodeDeviceConfiguration(obj, variables) +{ + var encoded = []; + var index = 0; + var config = {}; + var param = ""; + var value = 0; + try + { + for(var i=0; i= config.MIN && value <= config.MAX) + { + encoded[index] = CONFIG_DEVICE.REGISTER_CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + if(config.SIZE == 2) + { + if(config.MAX != 65535) + { + value = value * 100; + } + value = parseInt(value.toFixed(0)); + encoded[index] = (value >> 8) & 0xFF; + index = index + 1; + encoded[index] = value & 0xFF; + index = index + 1; + }else + { + encoded[index] = value; + index = index + 1; + } + }else + { + // Error + return []; + } + } + }catch(error) + { + // Error + return []; + } + return encoded; +} + +function encodeOpenthermConfiguration(obj, variables) +{ + var encoded = [] + var index = 0; + var firstType = parseInt("0x64", 16); + var field = ["BufferInterval", "UplinkInterval", "Mode", "Status", "DataIds"]; + var fieldIndex = 0; + var isFieldPresent = false; + var value = 0; + var dataIds = []; + var dataId = 0; + try + { + // Encode BufferInterval, UplinkInterval, Mode, Status + for(fieldIndex=0; fieldIndex<4; fieldIndex=fieldIndex+1) + { + isFieldPresent = false; + if(field[fieldIndex] in obj) + { + isFieldPresent = true; + } + if(!isFieldPresent) + { + return []; // error + } + value = obj[field[fieldIndex]]; + if((fieldIndex < 2 && value >= 1 && value <= 65535) || + (fieldIndex >= 2 && value >= 0 && value <= 1)) + { + encoded[index] = CONFIG_DEVICE.OPENTHERM_CHANNEL; + index = index + 1; + encoded[index] = firstType + fieldIndex; + index = index + 1; + if(fieldIndex < 2) // 2 bytes encoded + { + encoded[index] = (value >> 8) % 256; + index = index + 1; + encoded[index] = value % 256; + index = index + 1; + }else // 1 byte encoded + { + encoded[index] = value; + index = index + 1; + } + }else + { + // Error + return []; + } + } + // Encode dataIds + isFieldPresent = false; + if(field[fieldIndex] in obj) + { + isFieldPresent = true; + } + if(!isFieldPresent) + { + return []; // error + } + dataIds = obj[field[fieldIndex]]; + if(dataIds.length < 0 || dataIds.length > CONFIG_DEVICE.DATA_MAX_SIZE) + { + return []; // Error + } + encoded[index] = CONFIG_DEVICE.OPENTHERM_CHANNEL; + index = index + 1; + encoded[index] = firstType + fieldIndex; + index = index + 1; + var savedIndex = index; + for(var i=0; i CONFIG_DEVICE.DATA_MAX_SIZE) + { + return []; // error + } + encoded[index] = CONFIG_DEVICE.REGISTER_CHANNEL; + index = index + 1; + encoded[index] = CONFIG_DEVICE.READING_TYPE; + index = index + 1; + for(var i=0; i + * @version 2.0.0 + * @copyright YOBIIQ B.V. | https://www.yobiiq.com + * + * @release 2024-01-11 + * @update 2024-12-11 + * + * @product P1002003 iQ RM200 (iQ Digital Controller) + * + * @firmware RM200 firmware version >= 2.1 + * + */ + +// Version Control +var VERSION_CONTROL = { + CODEC : {VERSION: "2.0.0", NAME: "codecVersion"}, + DEVICE: {MODEL : "RM200", NAME: "genericModel"}, + PRODUCT: {CODE : "P1002003", 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 : 5, + }, + // alarm data + ALARM_DATA : { + CHANNEL : 170, // 0xAA + FPORT : 11, + }, + // parameter data + PARAMTER_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", + }, + 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" : "NORMAL MODE", "0x01" : "BUTTON 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 : "batteryVoltage", RESOLUTION: 0.1}, + "0x6D" : {SIZE : 1, NAME : "batteryPercentage"}, + "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 = { + "0x1A" : {SIZE: 1, NAME : "channel1State", + VALUES: { "0x00": "OFF", "0x01": "ON",} + }, + "0x1B" : {SIZE: 1, NAME : "channel1Control", + VALUES: { "0x00": "OFF", "0x01": "ON",} + }, + "0x1C" : {SIZE: 4, NAME : "channel1Counter",}, + "0x1D" : {SIZE: 1, NAME : "channel1DefaultState", + VALUES: {"0x00": "OFF", "0x01": "ON", "0x02": "RETAIN"} + }, + "0x1E" : {SIZE: 1, NAME : "channel1WatchdogState", + VALUES: {"0x00": "OFF", "0x01": "ON", "0x02": "RETAIN"} + }, + "0x1F" : {SIZE: 1, NAME : "channel1ButtonOverrideFunction", + VALUES: {"0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + "0x10" : {SIZE: 1, NAME : "channel1ButtonOverrideStatus", + VALUES: {"0x00" : "NORMAL MODE", "0x01" : "MANUAL ON", "0x02" : "MANUAL OFF",} + }, + "0x2A" : {SIZE: 1, NAME : "channel2State", + VALUES: { "0x00": "OFF", "0x01": "ON",} + }, + "0x2B" : {SIZE: 1, NAME : "channel2Control", + VALUES: { "0x00": "OFF", "0x01": "ON",} + }, + "0x2C" : {SIZE: 4, NAME : "channel2Counter",}, + "0x2D" : {SIZE: 1, NAME : "channel2DefaultState", + VALUES: {"0x00": "OFF", "0x01": "ON", "0x02": "RETAIN"} + }, + "0x2E" : {SIZE: 1, NAME : "channel2WatchdogState", + VALUES: {"0x00": "OFF", "0x01": "ON", "0x02": "RETAIN"} + }, + "0x2F" : {SIZE: 1, NAME : "channel2ButtonOverrideFunction", + VALUES: {"0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + "0x20" : {SIZE: 1, NAME : "channel2ButtonOverrideStatus", + VALUES: {"0x00" : "NORMAL MODE", "0x01" : "MANUAL ON", "0x02" : "MANUAL OFF",} + }, +}; + + +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; + 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; + 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){ + 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; + index = index + reg.DATA_SIZE; + } + 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.PARAMTER_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; + 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(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.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; + } + // 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.PARAMTER_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", + 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 + }, + 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: } + + /* 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",}, + "batteryVoltage": {TYPE: 108, /* 0x6C */ RW:"R",}, + "batteryPercentage": {TYPE: 109, /* 0x6D */ RW:"R",}, + "rebootDevice": {TYPE: 111, /* 0x6F */ SIZE: 1, MIN: 1, MAX: 1, RW:"WRITE_ONLY",}, + "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: 164, /* 0xA5 */ SIZE: 2, MIN: 1, MAX: 65535, RW:"RW",}, + "dataRetransmission": {TYPE: 165, /* 0xA6 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "lorawanWatchdogAlarm": {TYPE: 166, /* 0xA7 */ SIZE: 1, MIN: 0, MAX: 1, RW:"R",}, + + /* specific registers */ + "channel1State": {TYPE: 26, /* 0x1A */ RW:"R",}, + "channel1Control": {TYPE: 27, /* 0x1B */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "channel1Counter": {TYPE: 28, /* 0x1C */ RW:"R",}, + "channel1DefaultState": {TYPE: 29, /* 0x1D */ SIZE: 1, MIN: 0, MAX: 2, RW:"RW",}, + "channel1WatchdogState": {TYPE: 30, /* 0x1E */ SIZE: 1, MIN: 0, MAX: 2, RW:"RW",}, + "channel1ButtonOverrideFunction": {TYPE: 31, /* 0x1F */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "channel1ButtonOverrideStatus": {TYPE: 16, /* 0x10 */ RW:"R",}, + "channel1ButtonOverrideReset": {TYPE: 17, /* 0x11 */ SIZE: 1, MIN: 1, MAX: 1, RW:"W",}, + "channel2State": {TYPE: 42, /* 0x2A */ RW:"R",}, + "channel2Control": {TYPE: 43, /* 0x2B */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "channel2Counter": {TYPE: 44, /* 0x2C */ RW:"R",}, + "channel2DefaultState": {TYPE: 45, /* 0x2D */ SIZE: 1, MIN: 0, MAX: 2, RW:"RW",}, + "channel2WatchdogState": {TYPE: 46, /* 0x2E */ SIZE: 1, MIN: 0, MAX: 2, RW:"RW",}, + "channel2ButtonOverrideFunction": {TYPE: 47, /* 0x2F */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "channel2ButtonOverrideStatus": {TYPE: 32, /* 0x20 */ RW:"R",}, + "channel2ButtonOverrideReset": {TYPE: 33, /* 0x21 */ SIZE: 1, MIN: 1, MAX: 1, RW:"W",}, + }, + ERRORS : { + CMD_INVALID: "Invalid command", + CMD_REGISTER_NOT_FOUND: "Register not found in the device registers", + 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.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 == 2){ + encoded.push((cmdObj.Value >> 8) & 255); + encoded.push(cmdObj.Value & 255); + }else{ + 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 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