diff --git a/vendors/yobiiq/codecs/dsmr.js b/vendors/yobiiq/codecs/dsmr.js new file mode 100644 index 0000000..f1a0cd1 --- /dev/null +++ b/vendors/yobiiq/codecs/dsmr.js @@ -0,0 +1,1357 @@ +/** + *__ _____ ____ ___ ___ ___ + *\ \ / / _ \| __ )_ _|_ _/ _ \ + * \ V / | | | _ \| | | | | | | + * | || |_| | |_) | | | | |_| | + * |_| \___/|____/___|___\__\_\ + * + * + * @brief This YOBIIQ JS payload decoder/encoder follows the LoRa Alliance Payload Codec API specs (TS013-1.0.0). + * + * @compatibility TTN, TTI, LORIOT, ThingPark, ChirpStack v3/v4 and any LNS that follows LoRa Alliance API specs. + * + * @author Fostin Kpodar + * @version 1.0.0 + * @copyright YOBIIQ B.V. | https://www.yobiiq.com + * + * @release 2025-08-20 + * @update 2025-02-04 + * + * @product P1002001 iQ DSMR (iQ DSMR Basic) + * + * @firmware DSMR firmware version >= 1.0 + * + */ + +// Version Control +var VERSION_CONTROL = { + CODEC : {VERSION: "1.0.0", NAME: "codecVersion"}, + DEVICE: {MODEL : "DSMR", NAME: "genericModel"}, + PRODUCT: {CODE : "P1002005", NAME: "productCode"}, + MANUFACTURER: {COMPANY : "YOBIIQ B.V.", NAME: "manufacturer"}, +}; + +var UPLINK = { + // generic data + GENERIC_DATA : { + CHANNEL : 255, // 0xFF + FPORT_MIN : 50, + FPORT_MAX : 99, + }, + // device data + DEVICE_DATA : { + CHANNEL : 1, // 0x01 + FPORT_MIN : 1, + FPORT_MAX : 10, + }, + // alarm data + ALARM_DATA : { + CHANNEL_BASE : 160, // 0xA0 + CHANNEL : 170, // 0xAA + FPORT : 11, + }, + // threshold historic data + HISTORIC_DATA : { + CHANNEL : 1, // 0x01 + FPORT : 20, + }, + // parameter data + PARAMETER_DATA : { + CHANNEL : 255, // 0xFF + FPORT : 100, + }, + // general + MAC : { + FPORT : 0, + MSG: "MAC COMMAND RECEIVED", + }, + OPTIONAL_KEYS : { // in DEVICE_GENERIC_REGISTERS or in DEVICE_SPECIFIC_REGISTERS + RESOLUTION: "RESOLUTION", + VALUES: "VALUES", + SIGNED: "SIGNED", + DIGIT: "DIGIT", + UNIT: "UNIT", + HEX: "HEX", + }, + COMMON_REGISTERS: { + "0xFE" : {SIZE: 4, NAME : "timestamp"}, + "0x01" : {SIZE: 4, NAME : "dataloggerTimestamp"}, + }, + DOWNLINK : { + SUCCESS : "DOWNLINK COMMAND SUCCEEDED", + FAILURE : "DOWNLINK COMMAND FAILED" + }, + ERRORS : { + CHANNEL: "Unknown channel ", + TYPE: "Unknown type ", + FPORT_INCORRECT: "Incorrect fPort", + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info", +}; + +var DEVICE_GENERIC_REGISTERS = { + "0x64" : {SIZE : 1, NAME : "deviceStatus", + VALUES : { "0x00" : "FACTORY MODE", "0x01" : "NORMAL MODE",}, + }, + "0x65" : {SIZE : 0, NAME : "manufacturer"}, // size to be determinated + "0x66" : {SIZE : 0, NAME : "originalEquipmentManufacturer"}, // size to be determinated + "0x67" : {SIZE : 0, NAME : "deviceModel"}, // size to be determinated + "0x68" : {SIZE : 4, NAME : "deviceSerialNumber"}, + "0x69" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false}, + "0x6A" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false}, + "0x6B" : {SIZE : 1, NAME : "externalPowerStatus", + VALUES : { "0x00" : "AC POWER OFF", "0x01" : "AC POWER ON",}, + }, + "0x6C" : {SIZE : 1, NAME : "batteryPercentage"}, + "0x6D" : {SIZE : 2, NAME : "batteryVoltage", RESOLUTION: 0.001}, + "0x78" : {SIZE: 1, NAME : "internalCircuitTemperatureAlarm", + VALUES: {"0x00" : "NORMAL", "0x01" : "ALARM",} + }, + "0x79" : {SIZE: 4, NAME : "internalCircuitTemperatureNumberOfAlarms",}, + "0x7A" : {SIZE: 2, NAME : "internalCircuitTemperature", RESOLUTION: 0.01, SIGNED: true}, + "0x7B" : {SIZE: 1, NAME : "internalCircuitHumidity",}, + "0x82" : {SIZE: 2, NAME : "ambientTemperature", RESOLUTION: 0.01, SIGNED: true}, + "0x83" : {SIZE: 1, NAME : "ambientHumidity",}, + "0x96" : {SIZE : 1, NAME : "joinStatus", + VALUES : { "0x00" : "OFFLINE", "0x01" : "ONLINE",}, + }, + "0x9D" : {SIZE: 1, NAME : "applicationPort",}, + "0x9E" : {SIZE: 1, NAME : "joinType", + VALUES : { "0x01" : "OTAA",}, + }, + "0x9F" : {SIZE : 1, NAME : "deviceClass", + VALUES : { "0x00" : "CLASS A", "0x01" : "CLASS B", "0x02" : "CLASS C",}, + }, + "0xA0" : {SIZE: 1, NAME: "adr", + VALUES: {"0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + "0xA1" : {SIZE: 1, NAME: "sf", + VALUES: { "0x00" : "SF12BW125", "0x01" : "SF11BW125", "0x02" : "SF10BW125", + "0x03" : "SF9BW125", "0x04" : "SF8BW125", "0x05" : "SF7BW125", "0x06" : "SF7BW250",} + }, + "0xA3" : {SIZE: 1, NAME: "radioMode", SIZE: 1, + VALUES: { "0x00" : "LoRaWAN", "0x01" : "iQ D2D", "0x02" : "LoRaWAN & iQ D2D",} + }, + "0xA4" : {SIZE: 1, NAME: "numberOfJoinAttempts"}, + "0xA5" : {SIZE: 2, NAME: "linkCheckTimeframe",}, + "0xA6" : {SIZE: 1, NAME: "dataRetransmission", + VALUES: { "0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + "0xA7" : {SIZE: 1, NAME: "lorawanWatchdogAlarm", + VALUES: { "0x00" : "NORMAL", "0x01" : "ALARM",} + }, +}; + +var DEVICE_SPECIFIC_REGISTERS = { + "0xB5" : {SIZE: 1, NAME: "serialWatchdogFunction", + VALUES: { "0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + "0xB6" : {SIZE: 2, NAME: "serialWatchdogTimeout",}, + "0xB7" : {SIZE: 1, NAME: "serialWatchdogAlarm", + VALUES: { "0x00" : "NORMAL", "0x01" : "ALARM",} + }, + "0xB0" : {SIZE: 1, NAME: "serialStopBits", + VALUES: { "0x00" : "STOP_BITS_1", "0x01" : "STOP_BITS_1_5", + "0x02": "STOP_BITS_2", "0x03": "STOP_BITS_2_5", + "0x04": "STOP_BITS_3", "0x05": "STOP_BITS_3_5", + "0x06": "STOP_BITS_4" + } + }, + "0xB1" : {SIZE: 1, NAME: "serialDataWidth",}, + "0xB2" : {SIZE: 1, NAME: "serialParity", + VALUES: { "0x00" : "NONE", "0x01" : "ODD", "0x02" : "EVEN",} + }, + "0xB3" : {SIZE: 4, NAME: "serialBaudRate"}, + "0xB4" : {SIZE: 1, NAME: "dsmrProfile", + VALUES: { "0x00" : "NL", "0x01" : "LUX",} + }, + "0xD1" : {SIZE : 4, NAME : "pulseCounterDryInput1",}, + "0xD2" : {SIZE : 4, NAME : "pulseCounterDryInput2",}, + "0xDD" : {SIZE : 16, NAME : "decryptionKey", HEX:true}, + "0xDE" : {SIZE : 1, NAME : "decryptionFunction", + VALUES: { "0x00" : "DISABLED", "0x01" : "ENABLED",} + }, + + "0x10" : {SIZE : 2, NAME : "p1Version", DIGIT: false}, + "0x11" : {SIZE : 4, NAME : "telegramTimestamp",}, + "0x12" : {SIZE : 0, NAME : "equipmentIdentifier",}, + "0x13" : {SIZE : 4, NAME : "electricityDeliveredToClient", UNIT : "Wh", ALIAS: "totalImportedActiveEnergy"}, + "0x14" : {SIZE : 4, NAME : "electricityDeliveredToClientT1", UNIT : "Wh",}, + "0x15" : {SIZE : 4, NAME : "electricityDeliveredToClientT2", UNIT : "Wh",}, + "0x16" : {SIZE : 4, NAME : "electricityDeliveredByClient", UNIT : "Wh", ALIAS: "totalExportedActiveEnergy"}, + "0x17" : {SIZE : 4, NAME : "electricityDeliveredByClientT1", UNIT : "Wh",}, + "0x18" : {SIZE : 4, NAME : "electricityDeliveredByClientT2", UNIT : "Wh",}, + "0x19" : {SIZE : 2, NAME : "tariffIndicator",}, + "0x1A" : {SIZE : 4, NAME : "electricityPowerDelivered", UNIT : "W", SIGNED : true, ALIAS: "totalImportedActivePower"}, + "0x1B" : {SIZE : 4, NAME : "electricityPowerReceived", UNIT : "W", SIGNED : true, ALIAS: "totalExportedActivePower"}, + "0x1C" : {SIZE : 4, NAME : "numberOfPowerFailures",}, + "0x1D" : {SIZE : 4, NAME : "numberOfLongPowerFailures",}, + "0x1E" : {SIZE : 0, NAME : "powerFailureEventLog", SINGLE_LOG_SIZE:8, LOG:true}, + "0x1F" : {SIZE : 4, NAME : "numberOfVoltageSagsL1",}, + "0x20" : {SIZE : 4, NAME : "numberOfVoltageSagsL2",}, + "0x21" : {SIZE : 4, NAME : "numberOfVoltageSagsL3",}, + "0x22" : {SIZE : 4, NAME : "numberOfVoltageSwellsL1",}, + "0x23" : {SIZE : 4, NAME : "numberOfVoltageSwellsL2",}, + "0x24" : {SIZE : 4, NAME : "numberOfVoltageSwellsL3",}, + "0x25" : {SIZE : 4, NAME : "voltageL1", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x26" : {SIZE : 4, NAME : "voltageL2", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x27" : {SIZE : 4, NAME : "voltageL3", UNIT : "V", RESOLUTION : 0.1, SIGNED : true,}, + "0x28" : {SIZE : 4, NAME : "currentL1", UNIT : "A", SIGNED : true,}, + "0x29" : {SIZE : 4, NAME : "currentL2", UNIT : "A", SIGNED : true,}, + "0x2A" : {SIZE : 4, NAME : "currentL3", UNIT : "A", SIGNED : true,}, + "0x2B" : {SIZE : 4, NAME : "activePowerDeliveredL1", UNIT : "W", SIGNED : true, ALIAS: "importedActivePowerL1"}, + "0x2C" : {SIZE : 4, NAME : "activePowerDeliveredL2", UNIT : "W", SIGNED : true, ALIAS: "importedActivePowerL2"}, + "0x2D" : {SIZE : 4, NAME : "activePowerDeliveredL3", UNIT : "W", SIGNED : true, ALIAS: "importedActivePowerL3"}, + "0x2E" : {SIZE : 4, NAME : "activePowerReceivedL1", UNIT : "W", SIGNED : true, ALIAS: "exportedActivePowerL1"}, + "0x2F" : {SIZE : 4, NAME : "activePowerReceivedL2", UNIT : "W", SIGNED : true, ALIAS: "exportedActivePowerL2"}, + "0x30" : {SIZE : 4, NAME : "activePowerReceivedL3", UNIT : "W", SIGNED : true, ALIAS: "exportedActivePowerL3"}, + "0x31" : {SIZE : 2, NAME : "deviceTypeOnChannel1",}, + "0x32" : {SIZE : 0, NAME : "equipmentIdentifierChannel1",}, + "0x33" : {SIZE : 8, NAME : "lastReadingOnChannel1",}, + "0x34" : {SIZE : 2, NAME : "deviceTypeOnChannel2",}, + "0x35" : {SIZE : 0, NAME : "equipmentIdentifierChannel2",}, + "0x36" : {SIZE : 8, NAME : "lastReadingOnChannel2",}, + "0x37" : {SIZE : 2, NAME : "deviceTypeOnChannel3",}, + "0x38" : {SIZE : 0, NAME : "equipmentIdentifierChannel3",}, + "0x39" : {SIZE : 8, NAME : "lastReadingOnChannel3",}, + "0x3A" : {SIZE : 2, NAME : "deviceTypeOnChannel4",}, + "0x3B" : {SIZE : 0, NAME : "equipmentIdentifierChannel4",}, + "0x3C" : {SIZE : 8, NAME : "lastReadingOnChannel4",}, + "0x3D" : {SIZE : 4, NAME : "totalImportedReactiveEnergy", UNIT : "Wh",}, + "0x3E" : {SIZE : 4, NAME : "totalExportedReactiveEnergy", UNIT : "Wh",}, + "0x3F" : {SIZE : 4, NAME : "totalImportedReactivePower", UNIT : "VAR",}, + "0x40" : {SIZE : 4, NAME : "totalExportedReactivePower", UNIT : "VAR",}, + "0x41" : {SIZE : 4, NAME : "activeThreshold", UNIT: "kVA", RESOLUTION : 0.1}, + "0x42" : {SIZE : 4, NAME : "maxImportedExportedCurrent", UNIT : "A", SIGNED : true,}, + "0x43" : {SIZE : 4, NAME : "totalImportedApparentPower", UNIT : "VA",}, + "0x44" : {SIZE : 4, NAME : "totalExportedApparentPower", UNIT : "VA",}, + "0x45" : {SIZE : 1, NAME : "breakerControlState",}, + "0x46" : {SIZE : 1, NAME : "relay1ControlState",}, + "0x47" : {SIZE : 1, NAME : "relay2ControlState",}, + "0x48": {SIZE : 4, NAME : "importedReactivePowerL1", UNIT : "VAR"}, + "0x49": {SIZE : 4, NAME : "importedReactivePowerL2", UNIT : "VAR"}, + "0x4A": {SIZE : 4, NAME : "importedReactivePowerL3", UNIT : "VAR"}, + "0x4B": {SIZE : 4, NAME : "exportedReactivePowerL1", UNIT : "VAR"}, + "0x4C": {SIZE : 4, NAME : "exportedReactivePowerL2", UNIT : "VAR"}, + "0x4D": {SIZE : 4, NAME : "exportedReactivePowerL3", UNIT : "VAR"}, + "0x4E": {SIZE : 1, NAME : "valvePositionGasChannel1",}, + "0x4F": {SIZE : 1, NAME : "valvePositionGasChannel2",}, + "0x50": {SIZE : 1, NAME : "valvePositionGasChannel3",}, + "0x51": {SIZE : 1, NAME : "valvePositionGasChannel4",}, +}; + + +function decodeGenericData(bytes) +{ + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + while(index < bytes.length) + { + var reg = {}; + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != UPLINK.GENERIC_DATA.CHANNEL){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of generic register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_GENERIC_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = DEVICE_GENERIC_REGISTERS[type]; + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + decoded[reg.NAME] = reg.DATA; + if(reg.ALIAS){ + decoded[reg.ALIAS] = reg.DATA; + } + index = index + reg.DATA_SIZE; + } + return decoded; +} + +function decodeDeviceData(bytes) +{ + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var reg = {}; + while(index < bytes.length) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != UPLINK.DEVICE_DATA.CHANNEL){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_SPECIFIC_REGISTERS)){ + if(!(type in DEVICE_GENERIC_REGISTERS)){ + if(!(type in UPLINK.COMMON_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = UPLINK.COMMON_REGISTERS[type]; + }else{ + reg = DEVICE_GENERIC_REGISTERS[type]; + } + }else{ + reg = DEVICE_SPECIFIC_REGISTERS[type]; + } + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + decoded[reg.NAME] = reg.DATA; + if(reg.ALIAS){ + decoded[reg.ALIAS] = reg.DATA; + } + index = index + reg.DATA_SIZE; + } + return decoded; +} + +function decodeAlarmData(bytes) +{ + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var reg = {}; + while(index < bytes.length) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if((channel & UPLINK.ALARM_DATA.CHANNEL_BASE) != UPLINK.ALARM_DATA.CHANNEL_BASE){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_SPECIFIC_REGISTERS)){ + if(!(type in DEVICE_GENERIC_REGISTERS)){ + if(!(type in UPLINK.COMMON_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = UPLINK.COMMON_REGISTERS[type]; + }else{ + reg = DEVICE_GENERIC_REGISTERS[type]; + } + }else{ + reg = DEVICE_SPECIFIC_REGISTERS[type]; + } + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + decoded[reg.NAME] = reg.DATA; + if(reg.ALIAS){ + decoded[reg.ALIAS] = reg.DATA; + } + index = index + reg.DATA_SIZE; + } + return decoded; +} + +function decodeHistoricData(bytes) +{ + var decoded = {}; + var channel = 0; + var type = ""; + var reg = {}; + // package timestamp + var index = 2; // skip first channel and type + var packageTimestamp = getValueFromBytesBigEndianFormat(bytes, index, 4); + var listOfMeasurements = []; + index = index + 4; + while(index < bytes.length) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if((channel & UPLINK.ALARM_DATA.CHANNEL_BASE) != UPLINK.ALARM_DATA.CHANNEL_BASE){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_SPECIFIC_REGISTERS)){ + if(!(type in DEVICE_GENERIC_REGISTERS)){ + if(!(type in UPLINK.COMMON_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = UPLINK.COMMON_REGISTERS[type]; + }else{ + reg = DEVICE_GENERIC_REGISTERS[type]; + } + }else{ + reg = DEVICE_SPECIFIC_REGISTERS[type]; + } + var logItem = {}; + var timestampDelta = getValueFromBytesBigEndianFormat(bytes, index, 2); + index = index + 2; + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + logItem.name = reg.NAME; + logItem.data = reg.DATA; + if(reg.ALIAS){ + logItem.alias = reg.ALIAS; + } + logItem.ts = packageTimestamp - timestampDelta; + index = index + reg.DATA_SIZE; + listOfMeasurements.push(logItem); + } + decoded.packageTimestamp = packageTimestamp; + decoded.listOfMeasurements = listOfMeasurements; + return decoded; +} + +function decodeParameterData(bytes) +{ + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var reg = {}; + while(index < bytes.length) + { + channel = bytes[index]; + index = index + 1; + // Channel checking + if(channel != UPLINK.PARAMETER_DATA.CHANNEL){ + channel = "0x" + byteToEvenHEX(channel); + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.CHANNEL + channel + index; + return decoded; + } + // Type of register + type = "0x" + byteToEvenHEX(bytes[index]); + index = index + 1; + if(!(type in DEVICE_SPECIFIC_REGISTERS)){ + if(!(type in DEVICE_GENERIC_REGISTERS)){ + index = " at index " + (index - 1); + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.TYPE + type + index; + return decoded; + } + reg = DEVICE_GENERIC_REGISTERS[type]; + }else{ + reg = DEVICE_SPECIFIC_REGISTERS[type]; + } + // Decoding + reg.CHANNEL = channel; + reg.TYPE = type; + reg.INDEX = index; + reg = decodeRegister(bytes, reg); + decoded[reg.NAME] = reg.DATA; + if(reg.ALIAS){ + decoded[reg.ALIAS] = reg.DATA; + } + index = index + reg.DATA_SIZE; + } + return decoded; +} + +/** Helper functions **/ + +function decodeRegister(bytes, reg) +{ + var data = 0; + reg.DATA_SIZE = reg.SIZE; + if(UPLINK.OPTIONAL_KEYS.DIGIT in reg) + { + if(reg.DIGIT == false){ + // Decode into "V" + DIGIT STRING + "." DIGIT STRING format + data = getDigitStringArrayNoFormat(bytes, reg.INDEX, reg.DATA_SIZE); + data = "V" + data[0] + "." + data[1]; + }else{ + // Decode into DIGIT STRING format + data = getDigitStringArrayEvenFormat(bytes, reg.INDEX, reg.DATA_SIZE); + data = data.toString().toUpperCase(); + } + reg.DATA = data; + return reg; + } + if(UPLINK.OPTIONAL_KEYS.HEX in reg) + { + data = getDigitStringArrayEvenFormat(bytes, reg.INDEX, reg.DATA_SIZE); + data = data.join('').toUpperCase(); + reg.DATA = data; + return reg; + } + if(reg.VALUES){ + // Decode into HEX byte (VALUES specified in reg.VALUES) + data = "0x" + byteToEvenHEX(bytes[reg.INDEX]); + data = reg.VALUES[data]; + reg.DATA = data; + return reg; + } + if(reg.LOG){ + // power event logs (byte order little-endian) + var index = reg.INDEX; + var decoded = []; + var len = getValueFromBytesLittleEndianFormat(bytes, index, 4); + index = index + 4; + reg.DATA_SIZE = reg.DATA_SIZE + 4; + for(var i=0; i bytes.length) + { + reg.DATA = decoded; + return reg; + } + var log = {}; + log.timestamp = getValueFromBytesLittleEndianFormat(bytes, index, 4); + log.duration = getValueFromBytesLittleEndianFormat(bytes, index+4, 4); + decoded.push(log); + reg.DATA_SIZE = reg.DATA_SIZE + 8; + index = index + 8; + } + reg.DATA = decoded; + return reg; + } + if(reg.DATA_SIZE == 8) + { + // Slave last reading decoding + var decoded = {}; + decoded.timestamp = getValueFromBytesBigEndianFormat(bytes, reg.INDEX, 4); + decoded.value = getValueFromBytesBigEndianFormat(bytes, reg.INDEX+4, 4); + reg.DATA = decoded; + return reg; + } + if(reg.DATA_SIZE == 0) + { + reg.DATA_SIZE = getSizeBasedOnChannel(bytes, reg.INDEX, reg.CHANNEL); + // Decode into STRING format + data = getStringFromBytesBigEndianFormat(bytes, reg.INDEX, reg.DATA_SIZE); + reg.DATA = data; + return reg; + } + + if(reg.NAME == "maxImportedExportedCurrent") + { + // Decode into 2xINT16 format + var decoded = {}; + var val = getValueFromBytesBigEndianFormat(bytes, reg.INDEX, 2); + val = getSignedIntegerFromInteger(val, 2); + decoded.maxImportedCurrent = val; + val = getValueFromBytesBigEndianFormat(bytes, reg.INDEX+2, 2); + val = getSignedIntegerFromInteger(val, 2); + decoded.maxExportedCurrent = val; + reg.DATA = decoded; + return reg; + } + // Decode into DECIMAL format + data = getValueFromBytesBigEndianFormat(bytes, reg.INDEX, reg.DATA_SIZE); + if(reg.SIGNED){ + data = getSignedIntegerFromInteger(data, reg.DATA_SIZE); + } + if(reg.RESOLUTION){ + data = data * reg.RESOLUTION; + data = parseFloat(data.toFixed(2)); + } + reg.DATA = data; + return reg; +} + +function getStringFromBytesBigEndianFormat(bytes, index, size) +{ + var value = ""; + for(var i=0; i=0; i=i-1) + { + value = value + String.fromCharCode(bytes[index+i]); + } + return value; +} + +function getValueFromBytesBigEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=0; i<(size-1); i=i+1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index+size-1]; + return (value >>> 0); // to unsigned +} + +function getValueFromBytesLittleEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=(size-1); i>0; i=i-1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index]; + return (value >>> 0); // to unsigned +} + +function getDigitStringArrayNoFormat(bytes, index, size) +{ + var hexString = [] + for(var i=0; i= UPLINK.GENERIC_DATA.FPORT_MIN && fPort <= UPLINK.GENERIC_DATA.FPORT_MAX){ + decoded = decodeGenericData(bytes); + }else if(fPort >= UPLINK.DEVICE_DATA.FPORT_MIN && fPort <= UPLINK.DEVICE_DATA.FPORT_MAX){ + decoded = decodeDeviceData(bytes); + }else if(fPort == UPLINK.ALARM_DATA.FPORT){ + decoded = decodeAlarmData(bytes); + }else if(fPort == UPLINK.HISTORIC_DATA.FPORT){ + decoded = decodeHistoricData(bytes); + }else if(fPort == UPLINK.PARAMETER_DATA.FPORT){ + decoded = decodeParameterData(bytes); + }else{ + decoded.fPort = fPort; + decoded[UPLINK.ERROR_NAME] = UPLINK.ERRORS.FPORT_INCORRECT; + } + decoded[VERSION_CONTROL.CODEC.NAME] = VERSION_CONTROL.CODEC.VERSION; + decoded[VERSION_CONTROL.DEVICE.NAME] = VERSION_CONTROL.DEVICE.MODEL; + decoded[VERSION_CONTROL.PRODUCT.NAME] = VERSION_CONTROL.PRODUCT.CODE; + decoded[VERSION_CONTROL.MANUFACTURER.NAME] = VERSION_CONTROL.MANUFACTURER.COMPANY; + return decoded; +} + +// Decode uplink function. (ChirpStack v4, TTN, TTI, LORIOT, ThingPark) +// +// Input is an object with the following fields: +// - bytes = Byte array containing the uplink payload, e.g. [255, 230, 255, 0] +// - fPort = Uplink fPort. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - data = Object representing the decoded payload. +function decodeUplink(input) { + var errors = []; + var warnings = []; + var decoded = Decode(input.fPort, input.bytes, input.variables); + if(UPLINK.ERROR_NAME in decoded){ + errors.push(decoded[UPLINK.ERROR_NAME]); + } + if(UPLINK.WARNING_NAME in decoded){ + warnings.push(decoded[UPLINK.WARNING_NAME]); + } + return { + data: decoded, + errors: errors, + warnings: warnings + }; +} + +/*************************************************************************************************************/ +// Constants for device downlink +var DEVICE = { + + DOWNLINK : { + TYPE : "Type", + CONFIG : "Config", + PERIODIC: "Periodic", + THRESHOLD: "Threshold", + READING : "Reading" + }, + CONFIG : { + FPORT: 50, + CHANNEL : 255, // 0xFF + REG_MIN_NUMBER : 1, // downlink min number of registers + REG_MAX_NUMBER : 10, // downlink max number of registers + }, + PERIODIC : { + FPORT_MIN: 1, + FPORT_MAX: 5, + CHANNEL : 255, // 0xFF + INTERVAL_TYPE : 20, // 0x14 + MODE_TYPE : 21, // 0x15 + STATUS_TYPE : 22, // 0x16 + REGISTERS_TYPE : 23, // 0x17 + REG_MIN_NUMBER : 1, // downlink min number of registers + REG_MAX_NUMBER : 10, // downlink max number of registers + }, + THRESHOLD : { + FPORT: 11, + CHANNEL : 255, // 0xFF + REGISTER_TYPE : 64, // 0x40 + OPERATION_TYPE : 65, // 0x41 + MIN_TYPE : 66, // 0x42 + MAX_TYPE : 67, // 0x43 + DELTA_TYPE : 68, // 0x44 + LOG_INTERVAL_TYPE : 69, // 0x45 + UPLINK_INTERVAL_TYPE : 70, // 0x46 + UPLINK_MODE_TYPE : 71, // 0x47 + + OPERATION_VALUES : { + "DISABLED": 0, // 0b0000 + "MIN": 1, // 0b0001 + "MAX": 2, // 0b0010 + "DELTA": 4, // 0b0100 + }, + }, + READING: { + FPORT: 100, + CHANNEL : 255, // 0xFF + TYPE : 204, // 0xCC + REG_MIN_NUMBER : 1, // downlink min number of registers + REG_MAX_NUMBER : 10, // downlink max number of registers + }, + + REGISTERS : { + /* device registers */ + // SIZE, MIN and MAX are required if the register is writable (RW permission is "W" or "RW") + // "registerName": {TYPE:
, RW: <"R"/"W"/"RW">, SIZE: , MIN: , MAX: } + // TH indicates that the register is a threshold register (used in threshold downlinks) + + /* generic registers */ + "deviceStatus": {TYPE: 100, /* 0x64 */ RW:"R",}, + "manufacturer": {TYPE: 101, /* 0x65 */ RW:"R",}, + "originalEquipmentManufacturer": {TYPE: 102, /* 0x66 */ RW:"R",}, + "deviceModel": {TYPE: 103, /* 0x67 */ RW:"R",}, + "deviceSerialNumber": {TYPE: 104, /* 0x68 */ RW:"R",}, + "firmwareVersion": {TYPE: 105, /* 0x69 */ RW:"R",}, + "hardwareVersion": {TYPE: 106, /* 0x6A */ RW:"R",}, + "externalPowerStatus": {TYPE: 107, /* 0x6B */ RW:"R",}, + "batteryPercentage": {TYPE: 108, /* 0x6C */ RW:"R",}, + "batteryVoltage": {TYPE: 109, /* 0x6D */ RW:"R",}, + "rebootDevice": {TYPE: 111, /* 0x6F */ SIZE: 1, MIN: 1, MAX: 1, RW:"W",}, + "internalCircuitTemperatureAlarm": {TYPE: 120, /* 0x78 */ RW:"R",}, + "internalCircuitTemperatureNumberOfAlarms": {TYPE: 121, /* 0x79 */ RW:"R",}, + "internalCircuitTemperature": {TYPE: 122, /* 0x7A */ RW:"R",}, + "internalCircuitHumidity": {TYPE: 123, /* 0x7B */ RW:"R",}, + "ambientTemperature": {TYPE: 130, /* 0x82 */ RW:"R",}, + "ambientHumidity": {TYPE: 131, /* 0x83 */ RW:"R",}, + "joinStatus": {TYPE: 150, /* 0x96 */ RW:"R",}, + "applicationPort": {TYPE: 157, /* 0x9D */ SIZE: 1, MIN: 50, MAX: 99, RW:"RW",}, + "joinType": {TYPE: 158, /* 0x9E */ RW:"RW",}, + "deviceClass": {TYPE: 159, /* 0x9F */ RW:"RW",}, + "adr": {TYPE: 160, /* 0xA0 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "sf": {TYPE: 161, /* 0xA1 */ SIZE: 1, MIN: 0, MAX: 6, RW:"RW",}, + "restartLoRaWAN": {TYPE: 162, /* 0xA2 */ SIZE: 1, MIN: 1, MAX: 1, RW:"W",}, + "radioMode": {TYPE: 163, /* 0xA3 */ SIZE: 1, MIN: 0, MAX: 2, RW:"RW",}, + "numberOfJoinAttempts": {TYPE: 164, /* 0xA4 */ SIZE: 1, MIN: 0, MAX: 255, RW:"RW",}, + "linkCheckTimeframe": {TYPE: 165, /* 0xA5 */ SIZE: 2, MIN: 1, MAX: 65535, RW:"RW",}, + "dataRetransmission": {TYPE: 166, /* 0xA6 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "lorawanWatchdogAlarm": {TYPE: 167, /* 0xA7 */ SIZE: 1, MIN: 0, MAX: 1, RW:"R",}, + "serialWatchdogFunction": {TYPE: 181, /* 0xB5 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "serialWatchdogTimeout": {TYPE: 182, /* 0xB6 */ SIZE: 2, MIN: 1, MAX: 65535, RW:"RW",}, + "serialWatchdogAlarm": {TYPE: 183, /* 0xB7 */ SIZE: 1, MIN: 0, MAX: 1, RW:"R",}, + "serialStopBits": {TYPE: 176, /* 0xB0 */ SIZE: 1, MIN: 0, MAX: 6, RW:"RW",}, + "serialDataWidth": {TYPE: 177, /* 0xB1 */ SIZE: 1, MIN: 5, MAX: 9, RW:"RW",}, + "serialParity": {TYPE: 178, /* 0xB2 */ SIZE: 1, MIN: 0, MAX: 2, RW:"RW",}, + "serialBaudRate": {TYPE: 179, /* 0xB3 */ SIZE: 4, MIN: 1200, MAX: 115200, RW:"RW",}, + "dsmrProfile": {TYPE: 180, /* 0xB4 */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + "decryptionKey": {TYPE: 221, /* 0xDD */ SIZE: 16, RW:"RW", HEX:true,}, + "decryptionFunction": {TYPE: 222, /* 0xDE */ SIZE: 1, MIN: 0, MAX: 1, RW:"RW",}, + + + /* specific registers */ + "p1Version": {TYPE: 16, /* 0x10 */ RW:"R",}, + "telegramTimestamp": {TYPE: 17, /* 0x11 */ RW:"R",}, + "equipmentIdentifier": {TYPE: 18, /* 0x12 */ RW:"R",}, + "electricityDeliveredToClient": {TYPE: 19, /* 0x13 */ RW:"R", TH:true,}, + "totalImportedActiveEnergy": {TYPE: 19, /* 0x13 */ RW:"R", TH:true, ALIAS:true,}, + "electricityDeliveredToClientT1": {TYPE: 20, /* 0x14 */ RW:"R", TH:true,}, + "electricityDeliveredToClientT2": {TYPE: 21, /* 0x15 */ RW:"R", TH:true,}, + "electricityDeliveredByClient": {TYPE: 22, /* 0x16 */ RW:"R", TH:true,}, + "totalExportedActiveEnergy": {TYPE: 22, /* 0x16 */ RW:"R", TH:true, ALIAS:true,}, + "electricityDeliveredByClientT1": {TYPE: 23, /* 0x17 */ RW:"R", TH:true,}, + "electricityDeliveredByClientT2": {TYPE: 24, /* 0x18 */ RW:"R", TH:true,}, + "tariffIndicator": {TYPE: 25, /* 0x19 */ RW:"R",}, + "electricityPowerDelivered": {TYPE: 26, /* 0x1A */ RW:"R", TH:true,}, + "totalImportedActivePower": {TYPE: 26, /* 0x1A */ RW:"R", TH:true, ALIAS:true,}, + "electricityPowerReceived": {TYPE: 27, /* 0x1B */ RW:"R", TH:true,}, + "totalExportedActivePower": {TYPE: 27, /* 0x1B */ RW:"R", TH:true, ALIAS:true,}, + "numberOfPowerFailures": {TYPE: 28, /* 0x1C */ RW:"R",}, + "numberOfLongPowerFailures": {TYPE: 29, /* 0x1D */ RW:"R",}, + "powerFailureEventLog": {TYPE: 30, /* 0x1E */ RW:"R",}, + "numberOfVoltageSagsL1": {TYPE: 31, /* 0x1F */ RW:"R",}, + "numberOfVoltageSagsL2": {TYPE: 32, /* 0x20 */ RW:"R",}, + "numberOfVoltageSagsL3": {TYPE: 33, /* 0x21 */ RW:"R",}, + "numberOfVoltageSwellsL1": {TYPE: 34, /* 0x22 */ RW:"R",}, + "numberOfVoltageSwellsL2": {TYPE: 35, /* 0x23 */ RW:"R",}, + "numberOfVoltageSwellsL3": {TYPE: 36, /* 0x24 */ RW:"R",}, + "voltageL1": {TYPE: 37, /* 0x25 */ RW:"R", TH:true,}, + "voltageL2": {TYPE: 38, /* 0x26 */ RW:"R", TH:true,}, + "voltageL3": {TYPE: 39, /* 0x27 */ RW:"R", TH:true,}, + "currentL1": {TYPE: 40, /* 0x28 */ RW:"R", TH:true,}, + "currentL2": {TYPE: 41, /* 0x29 */ RW:"R", TH:true,}, + "currentL3": {TYPE: 42, /* 0x2A */ RW:"R", TH:true,}, + "activePowerDeliveredL1": {TYPE: 43, /* 0x2B */ RW:"R", TH:true,}, + "importedActivePowerL1": {TYPE: 43, /* 0x2B */ RW:"R", TH:true, ALIAS:true,}, + "activePowerDeliveredL2": {TYPE: 44, /* 0x2C */ RW:"R", TH:true,}, + "importedActivePowerL2": {TYPE: 44, /* 0x2C */ RW:"R", TH:true, ALIAS:true,}, + "activePowerDeliveredL3": {TYPE: 45, /* 0x2D */ RW:"R", TH:true,}, + "importedActivePowerL3": {TYPE: 45, /* 0x2D */ RW:"R", TH:true, ALIAS:true,}, + "activePowerReceivedL1": {TYPE: 46, /* 0x2E */ RW:"R", TH:true,}, + "exportedActivePowerL1": {TYPE: 46, /* 0x2E */ RW:"R", TH:true, ALIAS:true,}, + "activePowerReceivedL2": {TYPE: 47, /* 0x2F */ RW:"R", TH:true,}, + "exportedActivePowerL2": {TYPE: 47, /* 0x2F */ RW:"R", TH:true, ALIAS:true,}, + "activePowerReceivedL3": {TYPE: 48, /* 0x30 */ RW:"R", TH:true,}, + "exportedActivePowerL3": {TYPE: 48, /* 0x30 */ RW:"R", TH:true, ALIAS:true,}, + "deviceTypeOnChannel1": {TYPE: 49, /* 0x31 */ RW:"R",}, + "equipmentIdentifierChannel1": {TYPE: 50, /* 0x32 */ RW:"R",}, + "lastReadingOnChannel1": {TYPE: 51, /* 0x33 */ RW:"R",}, + "deviceTypeOnChannel2": {TYPE: 52, /* 0x34 */ RW:"R",}, + "equipmentIdentifierChannel2": {TYPE: 53, /* 0x35 */ RW:"R",}, + "lastReadingOnChannel2": {TYPE: 54, /* 0x36 */ RW:"R",}, + "deviceTypeOnChannel3": {TYPE: 55, /* 0x37 */ RW:"R",}, + "equipmentIdentifierChannel3": {TYPE: 56, /* 0x38 */ RW:"R",}, + "lastReadingOnChannel3": {TYPE: 57, /* 0x39 */ RW:"R",}, + "deviceTypeOnChannel4": {TYPE: 58, /* 0x3A */ RW:"R",}, + "equipmentIdentifierChannel4": {TYPE: 59, /* 0x3B */ RW:"R",}, + "lastReadingOnChannel4": {TYPE: 60, /* 0x3C */ RW:"R",}, + "totalImportedReactiveEnergy": {TYPE: 61, /* 0x3D */ RW:"R",}, + "totalExportedReactiveEnergy": {TYPE: 62, /* 0x3E */ RW:"R",}, + "totalImportedReactivePower": {TYPE: 63, /* 0x3F */ RW:"R",}, + "totalExportedReactivePower": {TYPE: 64, /* 0x40 */ RW:"R",}, + "activeThreshold": {TYPE: 65, /* 0x41 */ RW:"R",}, + "maxImportedExportedCurrent": {TYPE: 66, /* 0x42 */ RW:"R",}, + "totalImportedApparentPower": {TYPE: 67, /* 0x43 */ RW:"R",}, + "totalExportedApparentPower": {TYPE: 68, /* 0x44 */ RW:"R",}, + "breakerControlState": {TYPE: 69, /* 0x45 */ RW:"R",}, + "relay1ControlState": {TYPE: 70, /* 0x46 */ RW:"R",}, + "relay2ControlState": {TYPE: 71, /* 0x47 */ RW:"R",}, + "importedReactivePowerL1": {TYPE: 72, /* 0x48 */ RW:"R",}, + "importedReactivePowerL2": {TYPE: 73, /* 0x49 */ RW:"R",}, + "importedReactivePowerL3": {TYPE: 74, /* 0x4A */ RW:"R",}, + "exportedReactivePowerL1": {TYPE: 75, /* 0x4B */ RW:"R",}, + "exportedReactivePowerL2": {TYPE: 76, /* 0x4C */ RW:"R",}, + "exportedReactivePowerL3": {TYPE: 77, /* 0x4D */ RW:"R",}, + "valvePositionGasChannel1": {TYPE: 78, /* 0x4E */ RW:"R",}, + "valvePositionGasChannel2": {TYPE: 79, /* 0x4F */ RW:"R",}, + "valvePositionGasChannel3": {TYPE: 80, /* 0x50 */ RW:"R",}, + "valvePositionGasChannel4": {TYPE: 81, /* 0x51 */ RW:"R",}, + }, + ERRORS : { + CMD_INVALID: "Invalid command", + CMD_REGISTER_NOT_FOUND: "Register not found in the device registers", + CMD_REGISTER_NOT_THRESHOLD: "Register is not a threshold register", + CMD_REGISTER_NOT_WRITABLE: "Register not writable", + CMD_REGISTER_NOT_READABLE: "Register not readable", + CMD_REGISTER_NUMBER_INVALID: "Invalid number of registers", + CMD_DATA_INVALID: "Invalid data in the command", + CMD_FPORT_INVALID: "Invalid fPort in the command", + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info", +}; + +/************************************************************************************************************/ + +// Encode encodes the given object into an array of bytes. (ChirpStack v3) +// - fPort contains the LoRaWAN fPort number +// - obj is an object, e.g. {"temperature": 22.5} +// - variables contains the device variables e.g. {"calibration": "3.5"} (both the key / value are of type string) +// The function must return an array of bytes, e.g. [225, 230, 255, 0] +function Encode(fPort, obj, variables) { + if(!(DEVICE.DOWNLINK.TYPE in obj)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please add " + DEVICE.DOWNLINK.TYPE + " to the command"; + return []; // error + } + if(obj[DEVICE.DOWNLINK.TYPE] == DEVICE.DOWNLINK.CONFIG){ + if(fPort != DEVICE.CONFIG.FPORT){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_FPORT_INVALID; + return []; // error + } + return encodeDeviceConfiguration(obj[DEVICE.DOWNLINK.CONFIG]); + }else if(obj[DEVICE.DOWNLINK.TYPE] == DEVICE.DOWNLINK.PERIODIC){ + if(fPort < DEVICE.PERIODIC.FPORT_MIN || fPort > DEVICE.PERIODIC.FPORT_MAX){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_FPORT_INVALID; + return []; // error + } + return encodeUplinkConfiguration(obj[DEVICE.DOWNLINK.PERIODIC]); + }else if(obj[DEVICE.DOWNLINK.TYPE] == DEVICE.DOWNLINK.THRESHOLD){ + if(fPort != DEVICE.THRESHOLD.FPORT){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_FPORT_INVALID; + return []; // error + } + return encodeThresholdConfiguration(obj[DEVICE.DOWNLINK.THRESHOLD]); + }else if(obj[DEVICE.DOWNLINK.TYPE] == DEVICE.DOWNLINK.READING){ + if(fPort != DEVICE.READING.FPORT){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_FPORT_INVALID; + return []; // error + } + return encodeParameterReading(obj[DEVICE.DOWNLINK.READING]); + } + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please check " + obj[DEVICE.DOWNLINK.TYPE] + " in the command"; + return []; // error +} + +// Encode downlink function. (ChirpStack v4 , TTN, TTI, LORIOT, ThingPark) +// +// Input is an object with the following fields: +// - data = Object representing the payload that must be encoded. +// - variables = Object containing the configured device variables. +// +// Output must be an object with the following fields: +// - bytes = Byte array containing the downlink payload. +function encodeDownlink(input) { + var fPort = DEVICE.CONFIG.FPORT; // by default use config fPort (50) + if(input.data.fPort) + { + fPort = input.data.fPort; + } + var errors = []; + var warnings = []; + var encoded = Encode(fPort, input.data, input.variables); + if(DEVICE.ERROR_NAME in DEVICE) + { + errors.push(DEVICE[DEVICE.ERROR_NAME]); + } + if(DEVICE.WARNING_NAME in DEVICE) + { + warnings.push(DEVICE[DEVICE.WARNING_NAME]); + } + return { + bytes: encoded, + fPort: fPort, + errors: errors, + warnings : warnings + }; +} + + +/************************************************************************************************************/ + + +function encodeDeviceConfiguration(cmdArray) +{ + var encoded = []; + var reg = {}; + var regName = ""; + + if(!(cmdArray)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please add " + DEVICE.DOWNLINK.CONFIG + " array to the command"; + return []; // error + } + if(cmdArray.length < DEVICE.CONFIG.REG_MIN_NUMBER || + cmdArray.length > DEVICE.CONFIG.REG_MAX_NUMBER){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_REGISTER_NUMBER_INVALID + + ": please check " + DEVICE.DOWNLINK.CONFIG + " in the command"; + return []; + } + + for(var i=0; i reg.MAX){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check " + regName + " in the command"; + return []; // error + } + encoded.push(DEVICE.CONFIG.CHANNEL); + encoded.push(reg.TYPE); + if(reg.SIZE == 4) + { + encoded.push((cmdObj.Value >> 24) & 255); + encoded.push((cmdObj.Value >> 16) & 255); + encoded.push((cmdObj.Value >> 8) & 255); + encoded.push(cmdObj.Value & 255); + }else if(reg.SIZE == 2){ + encoded.push((cmdObj.Value >> 8) & 255); + encoded.push(cmdObj.Value & 255); + }else if(reg.SIZE == 1){ + encoded.push(cmdObj.Value); + } + } + return encoded; +} + +function encodeUplinkConfiguration(cmdObj) +{ + var encoded = []; + var reg = {}; + var regName = ""; + + if(!(cmdObj)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please add " + DEVICE.DOWNLINK.PERIODIC + " object to the command"; + return []; // error + } + if(!("UplinkInterval" in cmdObj) || !("Mode" in cmdObj) || + !("Status" in cmdObj) || !("Registers" in cmdObj)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID; + return []; // error + } + // Encode UplinkInterval, Mode, Status + if(cmdObj.UplinkInterval < 0 || cmdObj.UplinkInterval > 65535){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check UplinkInterval in the command"; + return []; // error + } + encoded.push(DEVICE.PERIODIC.CHANNEL); + encoded.push(DEVICE.PERIODIC.INTERVAL_TYPE); + encoded.push((cmdObj.UplinkInterval >> 8) & 255); + encoded.push(cmdObj.UplinkInterval & 255); + + if(cmdObj.Mode < 0 || cmdObj.Mode > 1){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check Mode in the command"; + return []; // error + } + encoded.push(DEVICE.PERIODIC.CHANNEL); + encoded.push(DEVICE.PERIODIC.MODE_TYPE); + encoded.push(cmdObj.Mode); + + if(cmdObj.Status < 0 || cmdObj.Status > 1){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check Status in the command"; + return []; // error + } + encoded.push(DEVICE.PERIODIC.CHANNEL); + encoded.push(DEVICE.PERIODIC.STATUS_TYPE); + encoded.push(cmdObj.Status); + // Encode registers + if(cmdObj.Registers.length < DEVICE.PERIODIC.REG_MIN_NUMBER || + cmdObj.Registers.length > DEVICE.PERIODIC.REG_MAX_NUMBER){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_REGISTER_NUMBER_INVALID + + ": please check Registers in the command"; + return []; // Error + } + encoded.push(DEVICE.PERIODIC.CHANNEL); + encoded.push(DEVICE.PERIODIC.REGISTERS_TYPE); + for(var i=0; i 65535){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check LogInterval in the command"; + return []; // error + } + encoded.push(DEVICE.THRESHOLD.CHANNEL); + encoded.push(DEVICE.THRESHOLD.LOG_INTERVAL_TYPE); + encoded.push((cmdObj.LogInterval >> 8) & 255); + encoded.push(cmdObj.LogInterval & 255); + } + + // Encode Operation, MinThreshold, MaxThreshold, DeltaThreshold + var op = DEVICE.THRESHOLD.OPERATION_VALUES.DISABLED; + var thresholds = [ + { name: "MinThreshold", type: DEVICE.THRESHOLD.MIN_TYPE, + flag: DEVICE.THRESHOLD.OPERATION_VALUES.MIN, opKey: "MIN" }, + { name: "MaxThreshold", type: DEVICE.THRESHOLD.MAX_TYPE, + flag: DEVICE.THRESHOLD.OPERATION_VALUES.MAX, opKey: "MAX" }, + { name: "DeltaThreshold", type: DEVICE.THRESHOLD.DELTA_TYPE, + flag: DEVICE.THRESHOLD.OPERATION_VALUES.DELTA, opKey: "DELTA" } + ]; + if("Operation" in cmdObj){ + if(!Array.isArray(cmdObj.Operation)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check Operation in the command (it must be an array)"; + return []; // error + } + for(var i=0; i> 24) & 255); + encoded.push((value >> 16) & 255); + encoded.push((value >> 8) & 255); + encoded.push(value & 255); + } + if ((op & th.flag) === th.flag) { + if (!(th.name in cmdObj)) { + warnings += "Threshold operation includes " + th.opKey + + ", but " + th.name + " is not present in this command. "; + } + } + } + if(warnings != ""){ + DEVICE[DEVICE.WARNING_NAME] = warnings; + } + + // Encode UplinkInterval if available + if("UplinkInterval" in cmdObj){ + if(cmdObj.UplinkInterval < 0 || cmdObj.UplinkInterval > 65535){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check UplinkInterval in the command"; + return []; // error + } + encoded.push(DEVICE.THRESHOLD.CHANNEL); + encoded.push(DEVICE.THRESHOLD.UPLINK_INTERVAL_TYPE); + encoded.push((cmdObj.UplinkInterval >> 8) & 255); + encoded.push(cmdObj.UplinkInterval & 255); + } + + // Encode UplinkMode if available + if("UplinkMode" in cmdObj){ + if(cmdObj.UplinkMode < 0 || cmdObj.UplinkMode > 1){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_DATA_INVALID + + ": please check UplinkMode in the command"; + return []; // error + } + encoded.push(DEVICE.THRESHOLD.CHANNEL); + encoded.push(DEVICE.THRESHOLD.UPLINK_MODE_TYPE); + encoded.push(cmdObj.UplinkMode); + } + return encoded; +} + +function encodeParameterReading(cmdArray) +{ + var encoded = []; + var reg = {}; + var regName = ""; + if(!(cmdArray)){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_INVALID + + ": please add " + DEVICE.DOWNLINK.READING + " array to the command"; + return []; // error + } + if(cmdArray.length < DEVICE.READING.REG_MIN_NUMBER || + cmdArray.length > DEVICE.READING.REG_MAX_NUMBER){ + DEVICE[DEVICE.ERROR_NAME] = DEVICE.ERRORS.CMD_REGISTER_NUMBER_INVALID + + ": please check " + DEVICE.DOWNLINK.READING + " in the command"; + return []; // error + } + encoded.push(DEVICE.READING.CHANNEL); + encoded.push(DEVICE.READING.TYPE); + for(var i=0; i=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 + * @version 1.1.0 + * @copyright YOBIIQ B.V. | https://www.yobiiq.com + * + * @release 06/12/2023 + * @update 10/30/2024 + * + * @author Dominic Hakke + * // Changes in header of document, naming conventions changed. + * + * + * @product P1002015 iQ SD-1001 (Smoke Detector) + * + * + */ + +// Version Control +var VERSION_CONTROL = { + CODEC : {VERSION: "1.1.0", NAME: "codecVersion"}, + DEVICE: {MODEL : "SD-1001", NAME: "deviceModel"}, + PRODUCT: {CODE : "1002015", NAME: "productCode"}, + MANUFACTURER: {COMPANY : "YOBIIQ B.V.", NAME: "manufacturer"}, +} + +// Configuration constants for device basic info +var CONFIG_INFO = { + FPORT : 50, + CHANNEL : parseInt("0xFF", 16), + TYPES : { + "0x09" : {SIZE : 2, NAME : "hardwareVersion", DIGIT: false}, + "0x0A" : {SIZE : 2, NAME : "firmwareVersion", DIGIT: false}, + "0x16" : {SIZE : 5, NAME : "deviceSerialNumber", DIGIT: true}, + "0x0F" : {SIZE : 1, NAME : "deviceClass", + VALUES : { + "0x00" : "Class A", + "0x01" : "Class B", + "0x02" : "Class C", + }, + }, + "0x0B" : {SIZE : 1, NAME : "powerEvent", + VALUES : { + "0x00" : "AC Power Off", + "0x01" : "AC Power On", + }, + }, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +// Configuration constants for data registers + var CONFIG_DATA = { + FPORT : 8, + CHANNELS : { + "0x01" : {SIZE : 1, NAME : "batteryLevelInPercentage",}, + "0x02" : {SIZE : 1, NAME : "powerEvent", + VALUES : { + "0x00" : "AC Power Off", + "0x01" : "AC Power On", + }, + }, + "0x03" : {SIZE : 1, NAME : "lowBatteryAlarm", + VALUES : { + "0x00" : "Normal", + "0x01" : "Alarm", + }, + }, + "0x04" : {SIZE : 1, NAME : "faultAlarm", + VALUES : { + "0x00" : "Normal", + "0x01" : "Alarm", + }, + }, + "0x05" : {SIZE : 1, NAME : "smokeAlarm", + VALUES : { + "0x00" : "Normal", + "0x01" : "Alarm", + }, + }, + "0x06" : {SIZE : 1, NAME : "interconnectAlarm", + VALUES : { + "0x00" : "Normal", + "0x01" : "Alarm", + }, + }, + "0x07" : {SIZE : 1, NAME : "testButtonPressed", + VALUES : { + "0x00" : "Normal", + "0x01" : "Pushed", + }, + }, + }, + WARNING_NAME : "warning", + ERROR_NAME : "error", + INFO_NAME : "info" +} + +function isBasicInformation(bytes, fPort) +{ + if(fPort == CONFIG_INFO.FPORT) + { + return true; + } + // Example: ff090100 ff0a0102 ff162404152795 ff0f02 ff0b01 + if(bytes[0] == CONFIG_INFO.CHANNEL && + bytes[4] == CONFIG_INFO.CHANNEL && + bytes[8] == CONFIG_INFO.CHANNEL + ) + { + return true + } + return false; +} + +function decodeBasicInformation(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = 0; + var type = ""; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_INFO.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_INFO.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + channel = bytes[index]; + index = index + 1; + if(channel != CONFIG_INFO.CHANNEL) + { + continue; // next byte + } + // Type of basic information + type = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + var info = CONFIG_INFO.TYPES[type] + size = info.SIZE; + // Decoding + var value = 0; + if(size != 0) + { + if("DIGIT" in info) + { + if(info.DIGIT == false) + { + // Decode into "V" + DIGIT STRING + "." DIGIT STRING format + value = getDigitStringArrayNoFormat(bytes, index, size); + value = "V" + value[0] + "." + value[1]; + }else + { + // Decode into DIGIT STRING format + value = getDigitStringArrayEvenFormat(bytes, index, size).join(""); + value = parseInt(value, 10); + } + } + else if("VALUES" in info) + { + // Decode into HEX STRING (VALUES specified in CONFIG_INFO) + value = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + value = info.VALUES[value]; + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + decoded[info.NAME] = value; + index = index + size; + } + } + }catch(error) + { + decoded[CONFIG_INFO.ERROR_NAME] = error.message; + } + + return decoded; +} + +function decodeDeviceData(bytes) +{ + var LENGTH = bytes.length; + var decoded = {}; + var index = 0; + var channel = ""; + var type = 0; + var size = 0; + if(LENGTH == 1) + { + if(bytes[0] == 0) + { + decoded[CONFIG_DATA.INFO_NAME] = "Downlink command succeeded"; + + } else if(bytes[0] == 1) + { + decoded[CONFIG_DATA.WARNING_NAME] = "Downlink command failed"; + } + return decoded; + } + try + { + while(index < LENGTH) + { + // Channel of device data + channel = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + index = index + 1; + // Type of device data + type = bytes[index]; + index = index + 1; + + // No type checking + + var config = CONFIG_DATA.CHANNELS[channel] + size = config.SIZE; + // Decoding + var value = 0; + if("VALUES" in config) + { + // Decode into STRING (VALUES specified in CONFIG_DATA) + value = "0x" + toEvenHEX(bytes[index].toString(16).toUpperCase()); + value = config.VALUES[value]; + }else + { + // Decode into DECIMAL format + value = getValueFromBytesBigEndianFormat(bytes, index, size); + } + decoded[config.NAME] = value; + index = index + size; + } + }catch(error) + { + decoded[CONFIG_DATA.ERROR_NAME] = error.message; + } + return decoded; +} + +function getValueFromBytesBigEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=0; i<(size-1); i=i+1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index+size-1] + return (value >>> 0); // to unsigned +} + +function getValueFromBytesLittleEndianFormat(bytes, index, size) +{ + var value = 0; + for(var i=(size-1); i>0; i=i-1) + { + value = (value | bytes[index+i]) << 8; + } + value = value | bytes[index] + return (value >>> 0); // to unsigned +} + +function getDigitStringArrayNoFormat(bytes, index, size) +{ + var hexString = [] + for(var i=0; i= config["MIN"] && obj[field[1]] <= config["MAX"]) + { + encoded[index] = CONFIG_DEVICE.CHANNEL; + index = index + 1; + encoded[index] = config.TYPE; + index = index + 1; + if(config.SIZE == 1) + { + encoded[index] = value; + index = index + 1; + }else if(config.SIZE == 2) + { + switch(config.TYPE) + { + case 3: // reporting interval + var lowByte = value % 256; + encoded[index] = ((lowByte & parseInt("0x0F", 16)) << 4) + (lowByte >> 4); + index = index + 1; + encoded[index] = (value >> 8) % 256; + index = index + 1; + break; + default: + encoded[index] = (value >> 8) % 256; + index = index + 1; + encoded[index] = value % 256; + index = index + 1; + break; + } + } + }else + { + // Error + return []; + } + }catch(error) + { + // Error + return []; + } + return encoded; +} \ No newline at end of file diff --git a/vendors/yobiiq/codecs/test_decode_dsmr.json b/vendors/yobiiq/codecs/test_decode_dsmr.json new file mode 100644 index 0000000..24031f8 --- /dev/null +++ b/vendors/yobiiq/codecs/test_decode_dsmr.json @@ -0,0 +1,166 @@ +[ + { + "description": "fPort 0 - MAC command", + "input": { + "fPort": 0, + "bytes": [0] + }, + "expected": { + "data": { + "mac": "MAC COMMAND RECEIVED", + "fPort": 0, + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 50 - downlink success", + "input": { + "fPort": 50, + "bytes": [0] + }, + "expected": { + "data": { + "info": "DOWNLINK COMMAND SUCCEEDED", + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 50 - generic registers", + "input": { + "fPort": 50, + "bytes": [ + 255, 100, 1, + 255, 109, 12, 228, + 255, 159, 2, + 255, 160, 1 + ] + }, + "expected": { + "data": { + "deviceStatus": "NORMAL MODE", + "batteryVoltage": 3.3, + "deviceClass": "CLASS C", + "adr": "ENABLED", + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 3 - device data with aliases", + "input": { + "fPort": 3, + "bytes": [ + 1, 19, 0, 0, 48, 57, + 1, 26, 0, 0, 4, 76, + 1, 37, 0, 0, 8, 252, + 1, 40, 0, 0, 0, 25 + ] + }, + "expected": { + "data": { + "electricityDeliveredToClient": 12345, + "totalImportedActiveEnergy": 12345, + "electricityPowerDelivered": 1100, + "totalImportedActivePower": 1100, + "voltageL1": 230, + "currentL1": 25, + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 11 - alarm data", + "input": { + "fPort": 11, + "bytes": [ + 160, 122, 9, 41, + 161, 167, 1 + ] + }, + "expected": { + "data": { + "internalCircuitTemperature": 23.45, + "lorawanWatchdogAlarm": "ALARM", + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 20 - historic data with one measurement", + "input": { + "fPort": 20, + "bytes": [ + 1, 254, 0, 0, 3, 232, + 160, 19, 0, 10, 0, 0, 0, 100 + ] + }, + "expected": { + "data": { + "packageTimestamp": 1000, + "listOfMeasurements": [ + { + "name": "electricityDeliveredToClient", + "data": 100, + "alias": "totalImportedActiveEnergy", + "ts": 990 + } + ], + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + }, + { + "description": "fPort 100 - parameter data", + "input": { + "fPort": 100, + "bytes": [ + 255, 180, 1, + 255, 221, 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255 + ] + }, + "expected": { + "data": { + "dsmrProfile": "LUX", + "decryptionKey": "00112233445566778899AABBCCDDEEFF", + "codecVersion": "1.0.0", + "genericModel": "DSMR", + "productCode": "P1002005", + "manufacturer": "YOBIIQ B.V." + }, + "errors": [], + "warnings": [] + } + } +] \ No newline at end of file diff --git a/vendors/yobiiq/codecs/test_decode_em2101.json b/vendors/yobiiq/codecs/test_decode_em2101.json new file mode 100644 index 0000000..f68a53b --- /dev/null +++ b/vendors/yobiiq/codecs/test_decode_em2101.json @@ -0,0 +1,171 @@ +[ + { + "description": "fPort 0 - MAC command received", + "input": { + "fPort": 0, + "bytes": [0] + }, + "expected": { + "data": { + "mac": "MAC command received", + "fPort": 0, + "codecVersion": "1.0.1", + "genericModel": "EM2101", + "productCode": "P1002009", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Downlink command ACK (succeeded)", + "input": { + "fPort": 50, + "bytes": [0] + }, + "expected": { + "data": { + "info": "Downlink command succeeded", + "codecVersion": "1.0.1", + "genericModel": "EM2101", + "productCode": "P1002009", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Basic device information (hwVersion, fwVersion, serialNumber, deviceClass)", + "input": { + "fPort": 50, + "bytes": [ + 255, 9, 1, 8, + 255, 10, 1, 5, + 255, 22, 0, 1, 35, 69, + 255, 15, 0 + ] + }, + "expected": { + "data": { + "hardwareVersion": "V1.8", + "firmwareVersion": "V1.5", + "deviceSerialNumber": 74565, + "deviceClass": "Class A", + "codecVersion": "1.0.1", + "genericModel": "EM2101", + "productCode": "P1002009", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Relay status HIGH and AC power-on event", + "input": { + "fPort": 50, + "bytes": [ + 255, 0, 1, + 255, 11, 1 + ] + }, + "expected": { + "data": { + "relayStatus": "HIGH", + "powerEvent": "AC Power On", + "codecVersion": "1.0.1", + "genericModel": "EM2101", + "productCode": "P1002009", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Current limit fallback (10.0 A) and voltage limit fallback (260 V)", + "input": { + "fPort": 50, + "bytes": [ + 255, 60, 0, 100, + 255, 61, 1, 4 + ] + }, + "expected": { + "data": { + "currentLimitFallback": { "data": 10, "unit": "A" }, + "voltageLimitFallback": { "data": 260, "unit": "V" }, + "codecVersion": "1.0.1", + "genericModel": "EM2101", + "productCode": "P1002009", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 1 - Measurement data: index, timestamp, active energy, voltage (230V), current (5A), active power (1150W), frequency (50Hz)", + "input": { + "fPort": 1, + "bytes": [ + 1, 0, 0, 0, 0, 1, + 1, 1, 0, 0, 3, 232, + 1, 4, 0, 188, 97, 78, + 1, 12, 0, 0, 8, 252, + 1, 16, 0, 0, 19, 136, + 1, 20, 0, 0, 4, 126, + 1, 35, 19, 136 + ] + }, + "expected": { + "data": { + "index": 1, + "timestamp": 1000, + "activeEnergyImportL1T1": { "data": 12345678, "unit": "Wh" }, + "voltageL1N": { "data": 230, "unit": "V" }, + "currentL1": { "data": 5000, "unit": "mA" }, + "activePowerL1": { "data": 1150, "unit": "W" }, + "frequency": { "data": 50, "unit": "Hz" }, + "codecVersion": "1.0.1", + "genericModel": "EM2101", + "productCode": "P1002009", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 11 - Change of state: relay CLOSED, digital input OPEN", + "input": { + "fPort": 11, + "bytes": [ + 1, 1, 1, + 2, 2, 0 + ] + }, + "expected": { + "data": { + "relayStatus": "CLOSED", + "digitalInputStatus": "OPEN", + "codecVersion": "1.0.1", + "genericModel": "EM2101", + "productCode": "P1002009", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 60 - Event log: relay switched off due to over-current, with timestamp and current value", + "input": { + "fPort": 60, + "bytes": [ + 253, 1, 1, + 253, 3, 0, 0, 3, 232, + 253, 5, 0, 0, 19, 136 + ] + }, + "expected": { + "data": { + "relaySwitchingOffReason": "Due to too high current limit", + "relaySwitchOffTime": 1000, + "currentWhenRelaySwitchingOff": 5000, + "codecVersion": "1.0.1", + "genericModel": "EM2101", + "productCode": "P1002009", + "manufacturer": "YOBIIQ B.V." + } + } + } +] diff --git a/vendors/yobiiq/codecs/test_decode_em4301.json b/vendors/yobiiq/codecs/test_decode_em4301.json new file mode 100644 index 0000000..8c4df4e --- /dev/null +++ b/vendors/yobiiq/codecs/test_decode_em4301.json @@ -0,0 +1,228 @@ +[ + { + "description": "fPort 0 - MAC command received", + "input": { + "fPort": 0, + "bytes": [0] + }, + "expected": { + "data": { + "mac": "MAC command received", + "fPort": 0, + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Downlink command ACK (succeeded)", + "input": { + "fPort": 50, + "bytes": [0] + }, + "expected": { + "data": { + "info": "Downlink command succeeded", + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Downlink command ACK (failed)", + "input": { + "fPort": 50, + "bytes": [1] + }, + "expected": { + "data": { + "warning": "Downlink command failed", + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Basic device information (hwVersion V1.8, fwVersion V1.5, serial, Class A)", + "input": { + "fPort": 50, + "bytes": [ + 255, 9, 1, 8, + 255, 10, 1, 5, + 255, 22, 0, 1, 35, 69, + 255, 15, 0 + ] + }, + "expected": { + "data": { + "hardwareVersion": "V1.8", + "firmwareVersion": "V1.5", + "deviceSerialNumber": 74565, + "deviceClass": "Class A", + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - AC power-on event", + "input": { + "fPort": 50, + "bytes": [ + 255, 11, 1 + ] + }, + "expected": { + "data": { + "powerEvent": "AC Power On", + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Transformer ratios (primary CT=100, secondary CT=5)", + "input": { + "fPort": 50, + "bytes": [ + 255, 30, 0, 100, + 255, 31, 5 + ] + }, + "expected": { + "data": { + "primaryCurrentTransformerRatio": 100, + "secondaryCurrentTransformerRatio": 5, + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 50 - Device model string", + "input": { + "fPort": 50, + "bytes": [ + 255, 40, 69, 77, 52, 51, 48, 49 + ] + }, + "expected": { + "data": { + "deviceModel": "EM4301", + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 1 - 3-phase measurements: index, timestamp, L1/L2/L3 voltages (230V), L1/L2/L3 currents (5A), frequency (50Hz)", + "input": { + "fPort": 1, + "bytes": [ + 1, 0, 0, 0, 0, 1, + 1, 1, 0, 0, 3, 232, + 1, 12, 0, 0, 8, 252, + 1, 13, 0, 0, 8, 252, + 1, 14, 0, 0, 8, 252, + 1, 16, 0, 0, 19, 136, + 1, 17, 0, 0, 19, 136, + 1, 18, 0, 0, 19, 136, + 1, 35, 19, 136 + ] + }, + "expected": { + "data": { + "index": 1, + "timestamp": 1000, + "voltageL1N": { "data": 230, "unit": "V" }, + "voltageL2N": { "data": 230, "unit": "V" }, + "voltageL3N": { "data": 230, "unit": "V" }, + "currentL1": { "data": 5000, "unit": "mA" }, + "currentL2": { "data": 5000, "unit": "mA" }, + "currentL3": { "data": 5000, "unit": "mA" }, + "frequency": { "data": 50, "unit": "Hz" }, + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 1 - 3-phase active power per phase (1000/2000/3000 W) and total system power (6 kW, 6.0 kvar)", + "input": { + "fPort": 1, + "bytes": [ + 1, 20, 0, 0, 3, 232, + 1, 21, 0, 0, 7, 208, + 1, 22, 0, 0, 11, 184, + 1, 36, 0, 0, 0, 6, + 1, 37, 0, 0, 23, 112 + ] + }, + "expected": { + "data": { + "activePowerL1": { "data": 1000, "unit": "W" }, + "activePowerL2": { "data": 2000, "unit": "W" }, + "activePowerL3": { "data": 3000, "unit": "W" }, + "totalSystemActivePower": { "data": 6, "unit": "kW" }, + "totalSystemReactivePower": { "data": 6, "unit": "kvar" }, + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 1 - Active energy import L123 T1 and T2", + "input": { + "fPort": 1, + "bytes": [ + 1, 4, 0, 188, 97, 78, + 1, 5, 0, 0, 39, 16 + ] + }, + "expected": { + "data": { + "activeEnergyImportL123T1": { "data": 12345678, "unit": "Wh" }, + "activeEnergyImportL123T2": { "data": 10000, "unit": "Wh" }, + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "fPort 99 - Incorrect fPort", + "input": { + "fPort": 99, + "bytes": [0] + }, + "expected": { + "data": { + "error": "Incorrect fPort", + "fPort": 99, + "codecVersion": "1.0.1", + "genericModel": "EM4301", + "productCode": "P1002011", + "manufacturer": "YOBIIQ B.V." + } + } + } +] diff --git a/vendors/yobiiq/codecs/test_decode_sd1001.json b/vendors/yobiiq/codecs/test_decode_sd1001.json new file mode 100644 index 0000000..4a20d6e --- /dev/null +++ b/vendors/yobiiq/codecs/test_decode_sd1001.json @@ -0,0 +1,90 @@ +[ + { + "description": "Basic information on fPort 50", + "input": { + "fPort": 50, + "bytes": [ + 255, 9, 1, 0, + 255, 10, 1, 2, + 255, 22, 36, 4, 21, 39, 149, + 255, 15, 2, + 255, 11, 1 + ] + }, + "expected": { + "data": { + "hardwareVersion": "V1.0", + "firmwareVersion": "V1.2", + "deviceSerialNumber": 2404152795, + "deviceClass": "Class C", + "powerEvent": "AC Power On", + "codecVersion": "1.1.0", + "deviceModel": "SD-1001", + "productCode": "1002015", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "Telemetry on fPort 8", + "input": { + "fPort": 8, + "bytes": [ + 1, 0, 95, + 2, 0, 1, + 3, 0, 0, + 4, 0, 0, + 5, 0, 1, + 6, 0, 0, + 7, 0, 1 + ] + }, + "expected": { + "data": { + "batteryLevelInPercentage": 95, + "powerEvent": "AC Power On", + "lowBatteryAlarm": "Normal", + "faultAlarm": "Normal", + "smokeAlarm": "Alarm", + "interconnectAlarm": "Normal", + "testButtonPressed": "Pushed", + "codecVersion": "1.1.0", + "deviceModel": "SD-1001", + "productCode": "1002015", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "Downlink acknowledgement success", + "input": { + "fPort": 8, + "bytes": [0] + }, + "expected": { + "data": { + "info": "Downlink command succeeded", + "codecVersion": "1.1.0", + "deviceModel": "SD-1001", + "productCode": "1002015", + "manufacturer": "YOBIIQ B.V." + } + } + }, + { + "description": "Downlink acknowledgement failed", + "input": { + "fPort": 8, + "bytes": [1] + }, + "expected": { + "data": { + "warning": "Downlink command failed", + "codecVersion": "1.1.0", + "deviceModel": "SD-1001", + "productCode": "1002015", + "manufacturer": "YOBIIQ B.V." + } + } + } +] \ No newline at end of file diff --git a/vendors/yobiiq/codecs/test_encode_dsmr.json b/vendors/yobiiq/codecs/test_encode_dsmr.json new file mode 100644 index 0000000..15113e4 --- /dev/null +++ b/vendors/yobiiq/codecs/test_encode_dsmr.json @@ -0,0 +1,99 @@ +[ + { + "description": "Config downlink (default fPort 50)", + "input": { + "data": { + "Type": "Config", + "Config": [ + { "Param": "adr", "Value": 1 }, + { "Param": "linkCheckTimeframe", "Value": 300 } + ] + } + }, + "expected": { + "bytes": [255, 160, 1, 255, 165, 1, 44], + "fPort": 50, + "errors": [], + "warnings": [] + } + }, + { + "description": "Config downlink with HEX register", + "input": { + "data": { + "Type": "Config", + "fPort": 50, + "Config": [ + { "Param": "decryptionKey", "Value": "00112233445566778899AABBCCDDEEFF" } + ] + } + }, + "expected": { + "bytes": [255, 221, 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255], + "fPort": 50, + "errors": [], + "warnings": [] + } + }, + { + "description": "Periodic uplink configuration (fPort 2)", + "input": { + "data": { + "Type": "Periodic", + "fPort": 2, + "Periodic": { + "UplinkInterval": 600, + "Mode": 1, + "Status": 1, + "Registers": ["electricityDeliveredToClient", "voltageL1"] + } + } + }, + "expected": { + "bytes": [255, 20, 2, 88, 255, 21, 1, 255, 22, 1, 255, 23, 19, 37], + "fPort": 2, + "errors": [], + "warnings": [] + } + }, + { + "description": "Threshold configuration (fPort 11)", + "input": { + "data": { + "Type": "Threshold", + "fPort": 11, + "Threshold": { + "Register": "electricityPowerDelivered", + "Operation": ["MIN", "MAX"], + "MinThreshold": 1000, + "MaxThreshold": 2000, + "LogInterval": 60, + "UplinkInterval": 120, + "UplinkMode": 1 + } + } + }, + "expected": { + "bytes": [255, 64, 26, 255, 69, 0, 60, 255, 65, 3, 255, 66, 0, 0, 3, 232, 255, 67, 0, 0, 7, 208, 255, 70, 0, 120, 255, 71, 1], + "fPort": 11, + "errors": [], + "warnings": [] + } + }, + { + "description": "Parameter reading request (fPort 100)", + "input": { + "data": { + "Type": "Reading", + "fPort": 100, + "Reading": ["deviceStatus", "electricityDeliveredToClient", "voltageL1"] + } + }, + "expected": { + "bytes": [255, 204, 100, 19, 37], + "fPort": 100, + "errors": [], + "warnings": [] + } + } +] \ No newline at end of file diff --git a/vendors/yobiiq/codecs/test_encode_em2101.json b/vendors/yobiiq/codecs/test_encode_em2101.json new file mode 100644 index 0000000..1731be8 --- /dev/null +++ b/vendors/yobiiq/codecs/test_encode_em2101.json @@ -0,0 +1,127 @@ +[ + { + "description": "Config - Restart device", + "input": { + "data": { + "Type": "Config", + "Config": { "Param": "restart", "Value": 1 } + } + }, + "expected": { + "bytes": [255, 11, 1] + } + }, + { + "description": "Config - Set current limit fallback to 10.0 A (raw: 100)", + "input": { + "data": { + "Type": "Config", + "Config": { "Param": "currentLimitFallback", "Value": 100 } + } + }, + "expected": { + "bytes": [255, 50, 0, 100] + } + }, + { + "description": "Config - Set digital input enabled", + "input": { + "data": { + "Type": "Config", + "Config": { "Param": "digitalInput", "Value": 1 } + } + }, + "expected": { + "bytes": [8, 71, 1] + } + }, + { + "description": "Dynamic - Set current limit to 100.0 A (raw: 1000)", + "input": { + "data": { + "Type": "Dynamic", + "Dynamic": { "Param": "currentLimit", "Value": 1000 } + } + }, + "expected": { + "bytes": [1, 50, 3, 232] + } + }, + { + "description": "Dynamic - Set power limit to 5000 W", + "input": { + "data": { + "Type": "Dynamic", + "Dynamic": { "Param": "powerLimit", "Value": 5000 } + } + }, + "expected": { + "bytes": [1, 52, 19, 136] + } + }, + { + "description": "Relay - Close relay (relayCommand = 1)", + "input": { + "data": { + "Type": "Relay", + "Relay": { "Param": "relayCommand", "Value": 1 } + } + }, + "expected": { + "bytes": [7, 72, 1] + } + }, + { + "description": "Relay - Open relay (relayCommand = 0)", + "input": { + "data": { + "Type": "Relay", + "Relay": { "Param": "relayCommand", "Value": 0 } + } + }, + "expected": { + "bytes": [7, 72, 0] + } + }, + { + "description": "Relay - Set control mode to LoRa (controlMode = 1)", + "input": { + "data": { + "Type": "Relay", + "Relay": { "Param": "controlMode", "Value": 1 } + } + }, + "expected": { + "bytes": [7, 71, 1] + } + }, + { + "description": "Measure - Configure periodic package: interval 10, mode 0, status enabled, measurements: voltage, current, active power", + "input": { + "data": { + "Type": "Measure", + "Measure": { + "Interval": 10, + "Mode": 0, + "Status": 1, + "Measurement": ["voltageL1N", "currentL1", "activePowerL1"] + } + } + }, + "expected": { + "bytes": [255, 20, 10, 255, 21, 0, 255, 22, 1, 255, 23, 12, 16, 20] + } + }, + { + "description": "RequestSettings - Request current and voltage fallback limits", + "input": { + "data": { + "Type": "RequestSettings", + "RequestSettings": ["currentLimitFallback", "voltageLimitFallback"] + } + }, + "expected": { + "bytes": [2, 11, 60, 2, 11, 61] + } + } +] diff --git a/vendors/yobiiq/codecs/test_encode_em4301.json b/vendors/yobiiq/codecs/test_encode_em4301.json new file mode 100644 index 0000000..28d42fa --- /dev/null +++ b/vendors/yobiiq/codecs/test_encode_em4301.json @@ -0,0 +1,108 @@ +[ + { + "description": "Config - Restart device", + "input": { + "data": { + "Type": "Config", + "Config": { "Param": "restart", "Value": 1 } + } + }, + "expected": { + "bytes": [255, 11, 1] + } + }, + { + "description": "Config - Set primary current transformer ratio to 100", + "input": { + "data": { + "Type": "Config", + "Config": { "Param": "primaryCurrentTransformerRatio", "Value": 100 } + } + }, + "expected": { + "bytes": [255, 30, 0, 100] + } + }, + { + "description": "Config - Set secondary current transformer ratio to 5", + "input": { + "data": { + "Type": "Config", + "Config": { "Param": "secondaryCurrentTransformerRatio", "Value": 5 } + } + }, + "expected": { + "bytes": [255, 31, 5] + } + }, + { + "description": "Config - Set primary voltage transformer ratio to 400", + "input": { + "data": { + "Type": "Config", + "Config": { "Param": "primaryVoltageTransformerRatio", "Value": 400 } + } + }, + "expected": { + "bytes": [255, 32, 0, 0, 1, 144] + } + }, + { + "description": "Config - Set secondary voltage transformer ratio to 100", + "input": { + "data": { + "Type": "Config", + "Config": { "Param": "secondaryVoltageTransformerRatio", "Value": 100 } + } + }, + "expected": { + "bytes": [255, 33, 0, 100] + } + }, + { + "description": "Measure - Configure periodic package: interval 30, mode 0, status enabled, measurements: L1/L2/L3 voltages", + "input": { + "data": { + "Type": "Measure", + "Measure": { + "Interval": 30, + "Mode": 0, + "Status": 1, + "Measurement": ["voltageL1N", "voltageL2N", "voltageL3N"] + } + } + }, + "expected": { + "bytes": [255, 20, 30, 255, 21, 0, 255, 22, 1, 255, 23, 12, 13, 14] + } + }, + { + "description": "Measure - Configure periodic package: interval 60, measurements: index, timestamp, L123 active energy, L1/L2/L3 current", + "input": { + "data": { + "Type": "Measure", + "Measure": { + "Interval": 60, + "Measurement": ["index", "timestamp", "activeEnergyImportL123T1", "currentL1", "currentL2", "currentL3"] + } + } + }, + "expected": { + "bytes": [255, 20, 60, 255, 23, 0, 1, 4, 16, 17, 18] + } + }, + { + "description": "Measure - Set mode only (mode 1 = push on change)", + "input": { + "data": { + "Type": "Measure", + "Measure": { + "Mode": 1 + } + } + }, + "expected": { + "bytes": [255, 21, 1] + } + } +] diff --git a/vendors/yobiiq/codecs/test_encode_sd1001.json b/vendors/yobiiq/codecs/test_encode_sd1001.json new file mode 100644 index 0000000..1c013b0 --- /dev/null +++ b/vendors/yobiiq/codecs/test_encode_sd1001.json @@ -0,0 +1,62 @@ +[ + { + "description": "Set reporting interval to 300 seconds", + "input": { + "data": { + "Type": "Config", + "Config": { + "Param": "reportingInterval", + "Value": 300 + } + } + }, + "expected": { + "bytes": [255, 3, 194, 1] + } + }, + { + "description": "Enable smoke detector alarm", + "input": { + "data": { + "Type": "Config", + "Config": { + "Param": "smokeDetector", + "Value": 1 + } + } + }, + "expected": { + "bytes": [255, 0, 1] + } + }, + { + "description": "Silence buzzer for 60 seconds", + "input": { + "data": { + "Type": "Config", + "Config": { + "Param": "silenceBuzzer", + "Value": 60 + } + } + }, + "expected": { + "bytes": [255, 10, 0, 60] + } + }, + { + "description": "Enable confirmed uplink", + "input": { + "data": { + "Type": "Config", + "Config": { + "Param": "confirmedUplink", + "Value": 1 + } + } + }, + "expected": { + "bytes": [255, 1, 1] + } + } +] \ No newline at end of file diff --git a/vendors/yobiiq/devices/yobiiq-dsmr.toml b/vendors/yobiiq/devices/yobiiq-dsmr.toml new file mode 100644 index 0000000..85852e0 --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-dsmr.toml @@ -0,0 +1,13 @@ +[device] +id = "6714f4f8-7ec6-44ae-912c-d9dd6252ae66" +name = "YOBIIQ DSMR" +description = "The YOBIIQ DSMR reads the data output of a Smart Meter utilizing the DSMR protocol." + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classC.toml"] +codec = "dsmr.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/dsmr-smart-metering/" +documentation_url = "https://yobiiq.com/products/dsmr-smart-metering/" diff --git a/vendors/yobiiq/devices/yobiiq-em2101.toml b/vendors/yobiiq/devices/yobiiq-em2101.toml new file mode 100644 index 0000000..917a38c --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-em2101.toml @@ -0,0 +1,13 @@ +[device] +id = "09b110be-91d1-420e-8134-b37e630235bb" +name = "YOBIIQ EM2101" +description = "EM2101 is a 1phase Electricity Meter with built in relay" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classC.toml"] +codec = "em2101.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/electricity-meters/iq-em2101-electricity-meter/" +documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em2101-electricity-meter/" diff --git a/vendors/yobiiq/devices/yobiiq-em4301-ct.toml b/vendors/yobiiq/devices/yobiiq-em4301-ct.toml new file mode 100644 index 0000000..a96b7c1 --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-em4301-ct.toml @@ -0,0 +1,13 @@ +[device] +id = "1e66fe6a-93fc-4889-b4e0-6755554225c7" +name = "YOBIIQ EM4301-CT" +description = "EM4301-CT is a 3phase Electricity Meter with measurement through current transformers" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classC.toml"] +codec = "em4301.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-ct-electricity-meter/" +documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-ct-electricity-meter/" diff --git a/vendors/yobiiq/devices/yobiiq-em4301.toml b/vendors/yobiiq/devices/yobiiq-em4301.toml new file mode 100644 index 0000000..7ddb7be --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-em4301.toml @@ -0,0 +1,13 @@ +[device] +id = "995ec891-4630-4799-a7c9-45091c1afc63" +name = "YOBIIQ EM4301" +description = "EM4301 is a 3phase Electricity Meter for direct connection" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classC.toml"] +codec = "em4301.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-electricity-meter/" +documentation_url = "https://yobiiq.com/products/electricity-meters/iq-em4301-electricity-meter/" diff --git a/vendors/yobiiq/devices/yobiiq-sd1001-ac.toml b/vendors/yobiiq/devices/yobiiq-sd1001-ac.toml new file mode 100644 index 0000000..9a27f0f --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-sd1001-ac.toml @@ -0,0 +1,13 @@ +[device] +id = "a2c24182-cef3-4a07-9877-785eddb9d6d2" +name = "YOBIIQ SD1001-AC" +description = "SD1001 Smoke Detector powered by AC" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classA.toml"] +codec = "sd1001.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/smoke-detector/" +documentation_url = "https://yobiiq.com/products/smoke-detector/" diff --git a/vendors/yobiiq/devices/yobiiq-sd1001-dc.toml b/vendors/yobiiq/devices/yobiiq-sd1001-dc.toml new file mode 100644 index 0000000..504b567 --- /dev/null +++ b/vendors/yobiiq/devices/yobiiq-sd1001-dc.toml @@ -0,0 +1,13 @@ +[device] +id = "6c28ed5d-c13e-4a2d-a402-5e4f703761d7" +name = "YOBIIQ SD1001-DC" +description = "SD1001 Smoke Detector powered by DC" + +[[device.firmware]] +version = "1.0.0" +profiles = ["EU868-classA.toml"] +codec = "sd1001.js" + +[device.metadata] +product_url = "https://yobiiq.com/products/smoke-detector/" +documentation_url = "https://yobiiq.com/products/smoke-detector/" diff --git a/vendors/yobiiq/profiles/EU868-classA.toml b/vendors/yobiiq/profiles/EU868-classA.toml new file mode 100644 index 0000000..5e0a507 --- /dev/null +++ b/vendors/yobiiq/profiles/EU868-classA.toml @@ -0,0 +1,25 @@ +[profile] +id = "be14525a-0883-4901-8f4e-6dc85182ebb9" +vendor_profile_id = 5392 +region = "EU868" +mac_version = "1.0.4" +reg_params_revision = "RP002-1.0.3" +supports_otaa = true +supports_class_b = false +supports_class_c = false +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 0 diff --git a/vendors/yobiiq/profiles/EU868-classC.toml b/vendors/yobiiq/profiles/EU868-classC.toml new file mode 100644 index 0000000..1ae7731 --- /dev/null +++ b/vendors/yobiiq/profiles/EU868-classC.toml @@ -0,0 +1,25 @@ +[profile] +id = "0ac9fb62-4a66-4edf-b044-9f0fbee0a7ec" +vendor_profile_id = 5424 +region = "EU868" +mac_version = "1.0.4" +reg_params_revision = "RP002-1.0.3" +supports_otaa = true +supports_class_b = false +supports_class_c = true +max_eirp = 16 + +[profile.abp] +rx1_delay = 0 +rx1_dr_offset = 0 +rx2_dr = 0 +rx2_freq = 0 + +[profile.class_b] +timeout_secs = 0 +ping_slot_nb_k = 0 +ping_slot_dr = 0 +ping_slot_freq = 0 + +[profile.class_c] +timeout_secs = 120 diff --git a/vendors/yobiiq/vendor.toml b/vendors/yobiiq/vendor.toml new file mode 100644 index 0000000..9aebbd9 --- /dev/null +++ b/vendors/yobiiq/vendor.toml @@ -0,0 +1,8 @@ +[vendor] +id = "dac78711-5cc1-4682-99bc-e8088a555528" +name = "YOBIIQ" +vendor_id = 415 +ouis = ["FC48C9"] + +[vendor.metadata] +homepage = "https://www.yobiiq.com"