Plugin file
Plugin configuration file
{
"name": "greenme-cube",
"version": "1.0.0",
"description": "Indoor environment sensor",
"author": "Thinger.io",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/thinger-io/plugins.git",
"directory": "greenme-cube"
},
"metadata": {
"name": "Greenme CUBE",
"description": "Indoor environment sensor",
"image": "assets/greenme-cube.png",
"category": "devices",
"vendor": "greenme"
},
"resources": {
"products": [
{
"description": "Indoor environment sensor",
"enabled": true,
"name": "Greenme CUBE",
"product": "greenme_cube",
"profile": {
"api": {
"downlink": {
"enabled": true,
"handle_connectivity": false,
"request": {
"data": {
"path": "/downlink",
"payload": "{\n \"data\" : \"{{payload.data=\"\"}}\",\n \"port\" : {{payload.port=85}},\n \"priority\": {{payload.priority=3}},\n \"confirmed\" : {{payload.confirmed=false}},\n \"uplink\" : {{property.uplink}} \n}",
"payload_function": "",
"payload_type": "",
"plugin": "{{property.uplink.source}}",
"target": "plugin_endpoint"
}
}
},
"uplink": {
"device_id_resolver": "getId",
"enabled": true,
"handle_connectivity": true,
"request": {
"data": {
"payload": "{{payload}}",
"payload_function": "",
"payload_type": "source_payload",
"resource_stream": "uplink",
"target": "resource_stream"
}
}
}
},
"autoprovisions": {
"device_autoprovisioning": {
"config": {
"mode": "pattern",
"pattern": "greenme-cube-.*"
},
"enabled": true
}
},
"buckets": {
"greenme_cube_data": {
"backend": "mongodb",
"data": {
"payload": "{{payload}}",
"payload_function": "",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"source": "resource_stream"
},
"enabled": true,
"retention": {
"period": 3,
"unit": "months"
},
"tags": [
"environment",
"sensors"
]
}
},
"code": {
"code": "\nfunction getTemperature(payload) {\n return payload.temperature;\n}\n\nfunction getHygrometry(payload) {\n return payload.hygrometry;\n}\n\nfunction getLux(payload) {\n return payload.lux;\n}\n\nfunction getNoiseAvg(payload) {\n return payload.noiseAvg;\n}\n\nfunction getNoiseMax(payload) {\n return payload.noiseMax;\n}\n\nfunction getCO2(payload) {\n return payload.co2;\n}\n\nfunction getTVOC(payload) {\n return payload.tvoc;\n}\n\nfunction decodeThingerUplink(thingerData) {\n // 0. If data has already been decoded, we will return it\n if (thingerData.decodedPayload) return thingerData.decodedPayload;\n \n // 1. Extract and Validate Input\n // We need 'payload' (hex string) and 'fPort' (integer)\n const hexPayload = thingerData.payload || \"\";\n const port = thingerData.fPort || 1;\n\n // 2. Convert Hex String to Byte Array\n const bytes = [];\n for (let i = 0; i < hexPayload.length; i += 2) {\n bytes.push(parseInt(hexPayload.substr(i, 2), 16));\n }\n\n // 3. Dynamic Function Detection and Execution\n \n // CASE A: (The Things Stack v3)\n if (typeof decodeUplink === 'function') {\n try {\n const input = {\n bytes: bytes,\n fPort: port\n };\n var result = decodeUplink(input);\n \n if (result.data) return result.data;\n\n return result; \n } catch (e) {\n console.error(\"Error inside decodeUplink:\", e);\n throw e;\n }\n }\n\n // CASE B: Legacy TTN (v2)\n else if (typeof Decoder === 'function') {\n try {\n return Decoder(bytes, port);\n } catch (e) {\n console.error(\"Error inside Decoder:\", e);\n throw e;\n }\n }\n\n // CASE C: No decoder found\n else {\n throw new Error(\"No compatible TTN decoder function (decodeUplink or Decoder) found in scope.\");\n }\n}\n\n\n// TTN decoder\n/**\n * THIS SOFTWARE IS PROVIDED BY GREENME SAS AND ITS CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES,\n * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n * IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,\n * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;\n * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\n * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,\n * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n * */\n\n///////////////////////////////////\n// Helper class and definitions\n///////////////////////////////////\nvar cp437ToUnicode = [0x0000,0x0001,0x0002,0x0003,0x0004,0x0005,0x0006,0x0007,0x0008,0x0009,0x000a,0x000b,0x000c,0x000d,0x000e,0x000f,0x0010,0x0011,0x0012,0x0013,0x0014,0x0015,0x0016,0x0017,0x0018,0x0019,0x001a,0x001b,0x001c,0x001d,0x001e,0x001f,0x0020,0x0021,0x0022,0x0023,0x0024,0x0025,0x0026,0x0027,0x0028,0x0029,0x002a,0x002b,0x002c,0x002d,0x002e,0x002f,0x0030,0x0031,0x0032,0x0033,0x0034,0x0035,0x0036,0x0037,0x0038,0x0039,0x003a,0x003b,0x003c,0x003d,0x003e,0x003f,0x0040,0x0041,0x0042,0x0043,0x0044,0x0045,0x0046,0x0047,0x0048,0x0049,0x004a,0x004b,0x004c,0x004d,0x004e,0x004f,0x0050,0x0051,0x0052,0x0053,0x0054,0x0055,0x0056,0x0057,0x0058,0x0059,0x005a,0x005b,0x005c,0x005d,0x005e,0x005f,0x0060,0x0061,0x0062,0x0063,0x0064,0x0065,0x0066,0x0067,0x0068,0x0069,0x006a,0x006b,0x006c,0x006d,0x006e,0x006f,0x0070,0x0071,0x0072,0x0073,0x0074,0x0075,0x0076,0x0077,0x0078,0x0079,0x007a,0x007b,0x007c,0x007d,0x007e,0x007f,0x00c7,0x00fc,0x00e9,0x00e2,0x00e4,0x00e0,0x00e5,0x00e7,0x00ea,0x00eb,0x00e8,0x00ef,0x00ee,0x00ec,0x00c4,0x00c5,0x00c9,0x00e6,0x00c6,0x00f4,0x00f6,0x00f2,0x00fb,0x00f9,0x00ff,0x00d6,0x00dc,0x00a2,0x00a3,0x00a5,0x20a7,0x0192,0x00e1,0x00ed,0x00f3,0x00fa,0x00f1,0x00d1,0x00aa,0x00ba,0x00bf,0x2310,0x00ac,0x00bd,0x00bc,0x00a1,0x00ab,0x00bb,0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,0x2555,0x2563,0x2551,0x2557,0x255d,0x255c,0x255b,0x2510,0x2514,0x2534,0x252c,0x251c,0x2500,0x253c,0x255e,0x255f,0x255a,0x2554,0x2569,0x2566,0x2560,0x2550,0x256c,0x2567,0x2568,0x2564,0x2565,0x2559,0x2558,0x2552,0x2553,0x256b,0x256a,0x2518,0x250c,0x2588,0x2584,0x258c,0x2590,0x2580,0x03b1,0x00df,0x0393,0x03c0,0x03a3,0x03c3,0x00b5,0x03c4,0x03a6,0x0398,0x03a9,0x03b4,0x221e,0x03c6,0x03b5,0x2229,0x2261,0x00b1,0x2265,0x2264,0x2320,0x2321,0x00f7,0x2248,0x00b0,0x2219,0x00b7,0x221a,0x207f,0x00b2,0x25a0];\n\n/**\n * Main class for encoding message.\n */\nclass Encoder\n{\n MSG_LEN_SET_CALIB = 21;\n MSG_LEN_SET_CFG = 45;\n MSG_LEN_SET_DISPLAY = 38;\n MSG_LEN_RESET = 6;\n MSG_LEN_SET_VALUE = 10;\n MSG_LEN_SET_ALERT = 8;\n \n\n RADIOMSG_HEADER1 = \t\t0x65;\n RADIOMSG_HEADER2 = \t\t0xef; //0xee on devices with fw < 2.5\n\n RADIOCMD_SET_CALIB = \t0x04;\n RADIOCMD_SET_CFG =\t\t0x05;\n RADIOCMD_REQUEST_CFG = \t0x06;\n RADIOCMD_SET_DISPLAY = \t0x07;\n RADIOCMD_RESET = \t0x08;\n RADIOCMD_SET_VALUE = \t0x09;\n RADIOCMD_SET_ALERT = \t0x0A;\n\n TOGGLE_ACTION_IMG\t=\t0x00;\n TOGGLE_ACTION_TEXT\t=\t0x01; \n\n POLL_RESPONSE_HAPPY =\t0x00;\n POLL_RESPONSE_YESNO =\t0x01;\n POLL_ACK_OK =\t\t0x00;\n POLL_ACK_SENT =\t0x01;\n\n LANG_FR_FR = 0x00;\n LANG_EN_US = 0x01;\n LANG_EN_GB = 0x02;\n LANG_DE_DE = 0x03;\n LANG_NL_NL = 0x04;\n LANG_PT_PT = 0x05;\n LANG_ES_ES = 0x06;\n\n MAX_TEXT_LENGTH = 30;\n MAX_TOGGLE_TEXT_LENGTH = 10;\n\n VALUETYPE_TEMP\t= 1;\n VALUETYPE_HYGR\t= 2;\n VALUETYPE_DBA\t= 3;\n VALUETYPE_LUX = 4;\n VALUETYPE_COLORR = 5;\n VALUETYPE_COLORG = 6;\n VALUETYPE_COLORB = 7;\n VALUETYPE_COLORW = 8;\n\n constructor()\n { \n }\n\n\t/***\n\t * Calibrate device with deltas or gain to raw electronic measurement.\n\t * @param deltaTemp_deg\n\t * @param deltaHygr\n\t * @param gainLux\n\t * @param deltaDBA\n\t * @param deltaOctave1\n\t * @param deltaOctave2\n\t * @param deltaOctave3\n\t * @param deltaOctave4\n\t * @param deltaOctave5\n\t * @param deltaOctave6\n\t * @param deltaOctave7\n\t * @param deltaOctave8\n\t * @return\n\t */\n\tMakeMsgCalib(\n deltaTemp_deg,\n deltaHygr,\n gainLux,\n deltaDBA,\n gainColorR,\n gainColorG,\n gainColorB,\n gainColorW\n )\n {\n let i= 0;\n let buffer = new Array(this.MSG_LEN_SET_CALIB).fill(0);\n i=0;\n buffer[i] = this.RADIOMSG_HEADER1;\n i++;\n buffer[i] = this.RADIOMSG_HEADER2;\n i++;\n //cmd\n buffer[i] = this.RADIOCMD_SET_CALIB; \n i++;\n this.WriteInt16ToBuffer((deltaTemp_deg*100), buffer, i);\n i=i+2;\n this.WriteInt16ToBuffer((deltaHygr*100), buffer, i);\n i=i+2;\n this.WriteU16ToBuffer((gainLux*100), buffer, i);\n i=i+2;\n this.WriteInt16ToBuffer((deltaDBA*100), buffer, i);\n i=i+2;\n this.WriteU16ToBuffer((gainColorR*100), buffer, i);\n i=i+2;\n this.WriteU16ToBuffer((gainColorG*100), buffer, i);\n i=i+2;\n this.WriteU16ToBuffer((gainColorB*100), buffer, i);\n i=i+2;\n this.WriteInt16ToBuffer((gainColorW*100), buffer, i);\n i=i+2;\n \n //crc\n let crc = this.Crc16_ccit_false(buffer, i);\n this.WriteCrc16ToBuffer(crc, buffer, i); \n i=i+2;\n\n let result = {b64:null, hex:null};\n let str = '';\n for (let i=0; i<buffer.length; i++)\n {\n if (buffer[i] < 16)\n str += '0'; \n str += buffer[i].toString(16);\n }\n result.hex = str;\n str = String.fromCharCode.apply(String, buffer);\n result.b64 = btoa(str);\n return result;\n }\n\n /**\n * Calibrate a sensor. Output value will take given value.\n * @param {*} sensorType \n * @param {*} measure \n * @returns \n */\n MakeMsgSetValue(sensorType, measure)\n {\n if ((sensorType < 1) || (sensorType > 8))\n return;\n\n let i= 0;\n let buffer = new Array(this.MSG_LEN_SET_VALUE).fill(0);\n i=0;\n buffer[i] = this.RADIOMSG_HEADER1;\n i++;\n buffer[i] = this.RADIOMSG_HEADER2;\n i++;\n //cmd\n buffer[i] = this.RADIOCMD_SET_VALUE; \n i++;\n \n buffer[i] = sensorType;\n i++;\n this.WriteFloatToBuffer(measure, buffer, i);\n i=i+4;\n \n //crc\n let crc = this.Crc16_ccit_false(buffer, i);\n this.WriteCrc16ToBuffer(crc, buffer, i); \n i=i+2;\n\n let result = {b64:null, hex:null};\n let str = '';\n for (let i=0; i<buffer.length; i++)\n {\n if (buffer[i] < 16)\n str += '0'; \n str += buffer[i].toString(16);\n }\n result.hex = str;\n str = String.fromCharCode.apply(String, buffer);\n result.b64 = btoa(str);\n return result;\n }\n\n /**\n * Reset device.\n * @param {} factoryReset true to perform full reset\n * @returns \n */\n MakeMsgReset(factoryReset = false)\n {\n let i= 0;\n let buffer = new Array(this.MSG_LEN_RESET).fill(0);\n\n \n buffer[i] = this.RADIOMSG_HEADER1;\n i++;\n buffer[i] = this.RADIOMSG_HEADER2;\n i++;\n //cmd\n buffer[i] = this.RADIOCMD_RESET; \n i++;\n \n if (factoryReset)\n buffer[i] = 0x11;\n else\n buffer[i] = 0x01;\n i++;\n \n //crc\n let crc = this.Crc16_ccit_false(buffer, i);\n this.WriteCrc16ToBuffer(crc, buffer, i); \n i=i+2;\n\n let result = {b64:null, hex:null};\n let str = '';\n for (let i=0; i<buffer.length; i++)\n {\n if (buffer[i] < 16)\n str += '0'; \n str += buffer[i].toString(16);\n }\n result.hex = str;\n str = String.fromCharCode.apply(String, buffer);\n result.b64 = btoa(str);\n return result;\n }\n\n /**\n * Configure the device\n * @param {*} shortMsgInterval_min interval between short messages, in minutes \n * @param {*} longMsgInterval_min interval between long messages, in minutes\n * @param {*} showTemp true if temperature screen must be displayed\n * @param {*} showHygr true if hygrometry screen must be displayed\n * @param {*} showLux true if light screen must be displayed\n * @param {*} showNoise true if noise screen must be displayed\n * @param {*} showAir true if air quality screen must be displayed\n * @param {*} showToggle true if the device should react on toggle left/right\n * @param {*} BleBeaconEnabled //unused\n * @param {*} disableSound disable sound measurment (will send zeros)\n * @param {*} disableVOC disable voc measurement\n * @param {*} toggleModeText show text rather than smileys\n * @param {*} imgToggleLeft unused\n * @param {*} imgToggleRight unused\n * @param {*} imgToggleBack unused\n * @param {*} extSensorType none, co2 or pm\n * @param {*} textToggleLeft text to display on toggle left\n * @param {*} textToggleRight text to display on toggle right \n * @param {*} textAcknowledgment text to display after toggle end\n * @param {*} lang language\n * @param {*} eventMode unused\n * @param {*} eventFrom unused\n * @param {*} eventThreshold unused\n * @param {*} eventWindow_s unused\n * @returns \n */\n MakeMsgConfig(shortMsgInterval_min, longMsgInterval_min, showTemp, showHygr, showLux, showNoise, showAir, showToggle, BleBeaconEnabled, disableSound, disableVOC,\n toggleModeText, imgToggleLeft, imgToggleRight, imgToggleBack, extSensorType, textToggleLeft, textToggleRight, textAcknowledgment, \n lang, eventMode, eventFrom, eventThreshold, eventWindow_s)\n {\n let i= 0;\n let val;\n let buffer = new Array(this.MSG_LEN_SET_CFG).fill(0);\n\n \n buffer[i] = this.RADIOMSG_HEADER1;\n i++;\n buffer[i] = this.RADIOMSG_HEADER2;\n i++;\n //cmd\n buffer[i] = this.RADIOCMD_SET_CFG; \n i++;\n \n buffer[i] = (shortMsgInterval_min > 255)?255:shortMsgInterval_min;\n i++;\n buffer[i] = (longMsgInterval_min > 255)?255:longMsgInterval_min;\n i++;\n\n buffer [i] = 0;\n buffer[i] |= showTemp?0x80:0;\n buffer[i] |= showHygr?0x40:0;\n buffer[i] |= showLux?0x20:0;\n buffer[i] |= showNoise?0x10:0;\n buffer[i] |= showAir?0x08:0;\n buffer[i] |= showToggle?0x04:0;\n buffer[i] |= BleBeaconEnabled?0x02:0;\n buffer[i] |= disableSound?0x01:0;\n i++;\n \n buffer [i] = 0;\n buffer[i] |= disableVOC?0x80:0;\n buffer[i] |= toggleModeText?0x40:0;\n val = (imgToggleLeft>7)?7:imgToggleLeft;\n buffer[i] |= val << 3;\n val = (imgToggleRight>7)?7:imgToggleRight;\n buffer[i] |= val;\n i++;\n\n buffer[i] = 0;\n val = (imgToggleBack>7)?7:imgToggleBack;\n buffer[i] |= val << 5;\n buffer[i] |= extSensorType & 0x1f;\n i++;\n \n let cp437str = this.Utf8TextToCp437(textToggleLeft);\n for (let j=0; j < cp437str.length; j++)\n {\n buffer[i+j] = cp437str[j];\n if (j >= this.MAX_TOGGLE_TEXT_LENGTH - 1) \n break;\n }\n i+= this.MAX_TOGGLE_TEXT_LENGTH;\n\n cp437str = this.Utf8TextToCp437(textToggleRight);\n for (let j=0; j < cp437str.length; j++)\n {\n buffer[i+j] = cp437str[j];\n if (j >= this.MAX_TOGGLE_TEXT_LENGTH - 1) \n break;\n }\n i+= this.MAX_TOGGLE_TEXT_LENGTH;\n\n cp437str = this.Utf8TextToCp437(textAcknowledgment);\n for (let j=0; j < cp437str.length; j++)\n {\n buffer[i+j] = cp437str[j];\n if (j >= this.MAX_TOGGLE_TEXT_LENGTH - 1) \n break;\n }\n i+= this.MAX_TOGGLE_TEXT_LENGTH;\n\n\n switch (lang)\n {\n case \"FR_FR\":\n buffer[i] = this.LANG_FR_FR;\n break;\n case \"EN_US\":\n buffer[i] = this.LANG_EN_US;\n break;\n case \"EN_GB\":\n buffer[i] = this.LANG_EN_GB;\n break;\n case \"DE_DE\":\n buffer[i] = this.LANG_DE_DE;\n break;\n case \"NL_NL\":\n buffer[i] = this.LANG_NL_NL;\n break;\n case \"PT_PT\":\n buffer[i] = this.LANG_PT_PT;\n break;\n case \"ES_ES\":\n buffer[i] = this.LANG_ES_ES;\n break;\n default:\n buffer[i] = this.LANG_EN_GB;\n break;\n }\n i++;\n\n buffer[i] = eventMode & 0xff;\n i++;\n\n buffer[i] = eventFrom & 0xff;\n i++;\n \n buffer[i] = eventThreshold & 0xff;\n i++;\n \n buffer[i] = eventWindow_s & 0xff;\n i++;\n\n //crc\n let crc = this.Crc16_ccit_false(buffer, i);\n this.WriteCrc16ToBuffer(crc, buffer, i); \n i=i+2;\n\n let result = {b64:null, hex:null};\n let str = '';\n for (let i=0; i<buffer.length; i++)\n {\n if (buffer[i] < 16)\n str += '0'; \n str += buffer[i].toString(16);\n }\n result.hex = str;\n str = String.fromCharCode.apply(String, buffer);\n result.b64 = btoa(str);\n return result;\n }\n\n /**\n * Configure the cube's display (beta)\n * \n * @param {*} endOnToggle set to true if display should return to normal after toggle\n * @param {*} endOnReboot set to true if display should return to normal after reboot\n * @param {*} expiresAfter_h how many hours should this message to show\n * @param {*} repeat_every_h how often this message should repeat\n * @param {*} responseChoices smileys (0x00) or yes/no (0x01)\n * @param {*} acknowledgement ok (0x00) or sent (0x01)\n * @param {*} text text to display, 30 characters max\n * @returns \n */\n MakeMsgDisplay(endOnToggle, endOnReboot, expiresAfter_h, repeat_every_h, responseChoices, acknowledgement, text)\n {\n let i= 0;\n let buffer = new Array(this.MSG_LEN_SET_DISPLAY).fill(0);\n let totalMsgLength = this.MAX_TEXT_LENGTH + 7;\n\n for (i=0 ; i<totalMsgLength; i++)\n {\n buffer[i] = 0;\n }\n i=0;\n buffer[i] = this.RADIOMSG_HEADER1;\n i++;\n buffer[i] = this.RADIOMSG_HEADER2;\n i++;\n buffer[i] = this.RADIOCMD_SET_DISPLAY; \n i++;\n\n let cfg = 0;\n if (endOnToggle)\n cfg |= 0x80;\n if (endOnReboot)\n cfg |= 0x40;\n \n \n cfg |= (responseChoices & 0x7) << 3;\n cfg |= (acknowledgement & 0x7) ;\n cfg &= 0xff; //cleanup\n buffer[i] = cfg;\n i++;\n \n buffer[i] = expiresAfter_h & 0xff;\n i++;\n\n buffer[i] = repeat_every_h & 0xff;\n i++;\n\n let cp437str = this.Utf8TextToCp437(text);\n for (let j=0; j < cp437str.length; j++)\n {\n buffer[i+j] = cp437str[j];\n if (j >= this.MAX_TEXT_LENGTH - 1) \n break;\n }\n i+= this.MAX_TEXT_LENGTH;\n \n\n //crc\n let crc = this.Crc16_ccit_false(buffer, i);\n this.WriteCrc16ToBuffer(crc, buffer, i); \n i=i+2;\n\n let result = {b64:null, hex:null};\n let str = '';\n for (let i=0; i<buffer.length; i++)\n {\n if (buffer[i] < 16)\n str += '0'; \n str += buffer[i].toString(16);\n }\n result.hex = str;\n str = String.fromCharCode.apply(String, buffer);\n result.b64 = btoa(str);\n return result;\n \n }\n\n\n /**\n * Convert UTF8 string to CP437 char array\n * @param {*} utf8str \n * @returns \n */\n Utf8TextToCp437(utf8str)\n {\n let cp437 = new Array();\n let len = utf8str.length;\n for (let i=0; i<len; i++)\n {\n let code = utf8str.charCodeAt(i);\n for (let j=0; j<cp437ToUnicode.length; j++)\n {\n //find char\n if (cp437ToUnicode[j] == code)\n {\n if ((j >= 0x20) && (j <= 0xA9))\n cp437.push(j);\n break;\n }\n }\n\n }\n return cp437;\n }\n\n Base64ToHexString(b64Str)\n {\n var str = atob(b64Str);\n }\n\n /**\n * Calculate CRC16\n * @param {*} buffer \n * @param {*} Size \n * @returns \n */\n Crc16_ccit_false(buffer, Size)\n {\n var crcTable = [\n 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,\n 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,\n 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,\n 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,\n 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,\n 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,\n 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,\n 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,\n 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,\n 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,\n 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,\n 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,\n 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,\n 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,\n 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,\n 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,\n 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,\n 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,\n 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,\n 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,\n 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,\n 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,\n 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,\n 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,\n 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,\n 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,\n 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,\n 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,\n 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,\n 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,\n 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,\n 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0,\n ];\n var crc = 0xFFFF;\n var j, i,c;\n \n for (i = 0; i < Size; i++) {\n \n c = buffer[i];\n j = (c ^ (crc >> 8)) & 0xFF;\n crc = crcTable[j] ^ (crc << 8);\n }\n \n return crc & 0xffff;\n }\n\n\n /**\n * Write CRC value to the end of the byte array.\n * @param {*} value \n * @param {*} buffer \n * @param {*} pos \n */\n WriteCrc16ToBuffer(value, buffer, pos)\n {\n let u16 = new Uint16Array([value])[0]; \n buffer[pos+1] = u16 >> 8;\n buffer[pos] = u16 & 0x00ff;\n }\n\n /**\n * Add UINT16 to byte array\n * @param {*} value \n * @param {*} buffer \n * @param {*} pos \n */\n WriteU16ToBuffer(value, buffer, pos)\n {\n let u16Array = new Int16Array(1);\n u16Array[0] = value;\n let byteArray = new Uint8Array(u16Array.buffer);\n for (let i=0; i<2; i++)\n buffer[pos+(1-i)] = byteArray[i]; \n }\n\n /**\n * Add 16 bit int to byte array\n * @param {*} value \n * @param {*} buffer \n * @param {*} pos \n */\n WriteInt16ToBuffer(value, buffer, pos)\n {\n let shortArray = new Int16Array(1);\n shortArray[0] = value;\n let byteArray = new Uint8Array(shortArray.buffer);\n for (let i=0; i<2; i++)\n buffer[pos+(1-i)] = byteArray[i]; \n }\n\n /**\n * Add float to byte array.\n * @param {*} value \n * @param {*} buffer \n * @param {*} pos \n */\n WriteFloatToBuffer(value, buffer, pos)\n {\n let floatArray = new Float32Array(1);\n floatArray[0] = value;\n let byteArray = new Uint8Array(floatArray.buffer);\n for (let i=0; i<4; i++)\n buffer[pos+i] = byteArray[i];\n }\n}\n\n/////////////////////////////////\n// TTN Encode Decode\n/////////////////////////////////\n\nfunction decodeUplink(input) {\n var data = {};\n var errors = [];\n var warnings = [];\n\n //constants\n var MESSAGEV2_SHORT = 5;\n var MESSAGEV2_FULL = 6;\n var MESSAGEV2_FEEL = 7;\n var MESSAGEEXT_NONE = 0;\n var MESSAGEEXT_CO2ONLY = 1;\n var MESSAGEEXT_COVONLY = 2;\n var MESSAGEEXT_COV_CO2 = 3;\n var MESSAGEEXT_PMONLY = 4;\n var MESSAGEEXT_COV_PM = 5;\n var MESSAGEEXT_COV_CO2_PM = 6;\n \n var FEEL_UNHAPPY = 1;\n var FEEL_HAPPY = -1;\n var FEEL_UNKNOWN = 0;\n \n var ValidateMessageSize = function (bytes)\n {\n var msgSizes = new Map();\n msgSizes[MESSAGEV2_SHORT] = 11;\n msgSizes[MESSAGEV2_FULL] = 26;\n msgSizes[MESSAGEV2_FEEL] = 26;\n \n var msgExtSizes = new Map();\n msgExtSizes[MESSAGEEXT_NONE] = 0;\n msgExtSizes[MESSAGEEXT_CO2ONLY] = 2;\n msgExtSizes[MESSAGEEXT_COVONLY] = 2;\n msgExtSizes[MESSAGEEXT_COV_CO2] = 4;\n \n var msgType = bytes[0] & 0x0f;\n var msgSize = bytes.length;\n var bodySize = msgSizes[msgType];\n\n if (bodySize === undefined)\n {\n return false;\n }\n \n if (msgSize < bodySize)\n return false;\n else \n {\n var msgExtType = bytes[bodySize - 1];\n var extSize = msgExtSizes[msgExtType];\n if (extSize === undefined)\n {\n return false;\n }\n else \n {\n if (msgSize == bodySize + extSize)\n return true;\n else \n return false;\n }\n }\n };\n \n var bytes = input.bytes;\n if (!ValidateMessageSize(bytes))\n {\n errors.push(\"invalid message size\");\n }\n else \n {\n var i = 0;\n \n //decode header\n var header = bytes[0];\n i++;\n\n var val = header & 0xc0;\n if (val == 0x40)\n data.lastFeel = FEEL_UNHAPPY;\n else if (val == 0x80)\n data.lastFeel = FEEL_HAPPY;\n else \n data.lastFeel = FEEL_UNKNOWN;\n \n var status = (header & 0x30) >> 4;\n var messageType = header & 0x0f;\n\n //decode code data\n if ((messageType == MESSAGEV2_SHORT) || (messageType == MESSAGEV2_FULL) || (messageType == MESSAGEV2_FEEL))\n {\n data.temperature = (bytes[i+1]*256 + bytes[i])/100.0;\n i+=2;\n data.hygrometry = (bytes[i+1]*256 + bytes[i])/100.0;\n i+=2;\n data.noiseMax = bytes[i]/2;\n i++;\n data.noiseAvg = bytes[i]/2;\n i++;\n data.lux = (bytes[i+1]*256 + bytes[i]);\n i+=2;\n }\n\n //decode full et feel messages\n if ((messageType == MESSAGEV2_FULL) || (messageType == MESSAGEV2_FEEL))\n {\n data.lightColorR = bytes[i] & 0xff;\n i++;\n data.lightColorG = bytes[i] & 0xff;\n i++;\n data.lightColorB = bytes[i] & 0xff;\n i++;\n data.lightColorW = (bytes[i+1]*256 + bytes[i]);\n i=i+2;\n data.flicker = bytes[i];\n i++;\n data.octave1 = bytes[i];\n i++;\n data.octave2 = bytes[i];\n i++;\n data.octave3 = bytes[i];\n i++;\n data.octave4 = bytes[i];\n i++;\n data.octave5 = bytes[i];\n i++;\n data.octave6 = bytes[i];\n i++;\n data.octave7 = bytes[i];\n i++;\n data.octave8 = bytes[i];\n i++;\n data.octave9 = bytes[i];\n i++;\n }\n \n if ((messageType == MESSAGEV2_SHORT) || (messageType == MESSAGEV2_FULL) ||\n (messageType == MESSAGEV2_FEEL))\n {\n //battery level and status: unused\n i++;\n \n //extended messages\n var extMsgType = bytes[i];\n i++;\n \n //var extMessageType = extMsgType;\n if (extMsgType == MESSAGEEXT_CO2ONLY)\n {\n data.co2 = bytes[i+1]*256 + bytes[i];\n i+=2;\n }\n else if (extMsgType == MESSAGEEXT_COVONLY)\n {\n data.tvoc = bytes[i+1]*256 + bytes[i];\n i+=2;\n }\n else if (extMsgType == MESSAGEEXT_COV_CO2)\n {\n data.tvoc = bytes[i+1]*256 + bytes[i];\n i+=2;\n data.co2 = bytes[i+1]*256 + bytes[i];\n i+=2;\n }\n }\n }\n \n return {\n data: data,\n warnings: warnings,\n errors: errors\n };\n}\n\nfunction encodeDownlink(input) {\n let encoder = new Encoder();\n let bytes = null;\n\n if (input.msgType == 'MakeMsgCalib')\n {\n bytes = encoder.MakeMsgCalib(\n input.deltaTemp_deg,\n input.deltaHygr,\n input.gainLux,\n input.deltaDBA,\n input.gainColorR,\n input.gainColorG,\n input.gainColorB,\n input.gainColorW);\n }\n else if (input.msgType == 'MakeMsgSetValue')\n {\n bytes = encoder.MakeMsgSetValue(\n input.sensorType, \n input.measure);\n }\n else if (input.msgType == 'MakeMsgConfig')\n {\n bytes = encoder.MakeMsgConfig(\n input.shortMsgInterval_min, \n input.longMsgInterval_min, \n input.showTemp, \n input.showHygr, \n input.showLux, \n input.showNoise, \n input.showAir, \n input.showToggle, \n 0, \n input.disableSound, \n input.disableVOC,\n input.toggleModeText, \n input.imgToggleLeft, \n input.imgToggleRight, \n input.imgToggleBack, \n input.extSensorType, \n input.textToggleLeft, \n input.textToggleRight, \n input.textAcknowledgment, \n input.lang, \n 0, \n 0, \n 0, \n 0);\n \n }\n else if (input.msgType == 'MakeMsgDisplay')\n {\n bytes = encoder.MakeMsgDisplay(\n input.endOnToggle,\n input.endOnReboot, \n input.expiresAfter_h, \n input.repeat_every_h, \n input.responseChoices, \n input.acknowledgement, \n input.text);\n }\n else\n {\n return {errors:['bad message type.']};\n }\n\n if (bytes == null)\n {\n return {errors:['could not encode message.']};\n } \n else {\n return {\n // LoRaWAN FPort used for the downlink message\n fPort: 1,\n // Encoded bytes\n bytes: bytes,\n };\n }\n}\n\nfunction decodeDownlink(input) {\n return null;\n}\n",
"environment": "javascript",
"storage": "",
"version": "1.0"
},
"flows": {
"greenme_decoder_flow": {
"data": {
"payload": "{{payload}}",
"payload_function": "decodeThingerUplink",
"payload_type": "source_payload",
"resource": "uplink",
"source": "resource",
"update": "events"
},
"enabled": true,
"handle_connectivity": false,
"sink": {
"payload": "{{payload}}",
"payload_function": "",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"target": "resource_stream"
},
"split_data": false
}
},
"properties": {
"greenme_cube_co2": {
"data": {
"payload": "{{payload}}",
"payload_function": "getCO2",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"source": "resource_stream"
},
"default": {
"source": "value",
"value": 0
},
"enabled": true
},
"greenme_cube_hygrometry": {
"data": {
"payload": "{{payload}}",
"payload_function": "getHygrometry",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"source": "resource_stream"
},
"default": {
"source": "value",
"value": 0
},
"enabled": true
},
"greenme_cube_lux": {
"data": {
"payload": "{{payload}}",
"payload_function": "getLux",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"source": "resource_stream"
},
"default": {
"source": "value",
"value": 0
},
"enabled": true
},
"greenme_cube_noise_avg": {
"data": {
"payload": "{{payload}}",
"payload_function": "getNoiseAvg",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"source": "resource_stream"
},
"default": {
"source": "value",
"value": 0
},
"enabled": true
},
"greenme_cube_noise_max": {
"data": {
"payload": "{{payload}}",
"payload_function": "getNoiseMax",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"source": "resource_stream"
},
"default": {
"source": "value",
"value": 0
},
"enabled": true
},
"greenme_cube_temperature": {
"data": {
"payload": "{{payload}}",
"payload_function": "getTemperature",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"source": "resource_stream"
},
"default": {
"source": "value",
"value": 0
},
"enabled": true
},
"greenme_cube_tvoc": {
"data": {
"payload": "{{payload}}",
"payload_function": "getTVOC",
"payload_type": "source_payload",
"resource_stream": "uplink_decoded",
"source": "resource_stream"
},
"default": {
"source": "value",
"value": 0
},
"enabled": true
},
"uplink": {
"data": {
"payload": "{{payload}}",
"payload_function": "",
"payload_type": "source_payload",
"resource": "uplink",
"source": "resource",
"update": "events"
},
"default": {
"source": "value"
},
"description": "Last Raw uplink",
"enabled": true
}
}
},
"_resources": {
"properties": [
{
"description": "",
"name": "",
"property": "greenme_cube_tvoc",
"type": "",
"value": 0
},
{
"description": "",
"name": "",
"property": "greenme_cube_temperature",
"type": "",
"value": 0
},
{
"description": "",
"name": "",
"property": "greenme_cube_noise_max",
"type": "",
"value": 0
},
{
"description": "",
"name": "",
"property": "greenme_cube_noise_avg",
"type": "",
"value": 0
},
{
"description": "",
"name": "",
"property": "greenme_cube_lux",
"type": "",
"value": 0
},
{
"description": "",
"name": "",
"property": "greenme_cube_hygrometry",
"type": "",
"value": 0
},
{
"description": "",
"name": "",
"property": "greenme_cube_co2",
"type": "",
"value": 0
},
{
"property": "dashboard",
"value": {
"tabs": [
{
"name": "Environment Monitor",
"widgets": [
{
"layout": {
"col": 0,
"row": 0,
"sizeX": 2,
"sizeY": 6
},
"panel": {
"color": "#ffffff",
"currentColor": "#ffffff",
"showOffline": {
"type": "none"
},
"title": "Temperature"
},
"properties": {
"color": "#ff0000",
"max": 50,
"min": -10,
"unit": "°C"
},
"sources": [
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "temperature",
"tags": {
"device": [],
"group": []
}
},
"color": "#e74c3c",
"name": "Temperature",
"source": "bucket",
"timespan": {
"mode": "latest"
}
}
],
"type": "donutchart"
},
{
"layout": {
"col": 2,
"row": 0,
"sizeX": 2,
"sizeY": 6
},
"panel": {
"color": "#ffffff",
"currentColor": "#ffffff",
"showOffline": {
"type": "none"
},
"title": "Humidity"
},
"properties": {
"color": "#0000ff",
"max": 100,
"min": 0,
"unit": "%"
},
"sources": [
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "hygrometry",
"tags": {
"device": [],
"group": []
}
},
"color": "#3498db",
"name": "Humidity",
"source": "bucket",
"timespan": {
"mode": "latest"
}
}
],
"type": "donutchart"
},
{
"layout": {
"col": 0,
"row": 6,
"sizeX": 2,
"sizeY": 6
},
"panel": {
"color": "#ffffff",
"currentColor": "#ffffff",
"showOffline": {
"type": "none"
},
"title": "Light Level"
},
"properties": {
"color": "#ffcc00",
"max": 1000,
"min": 0,
"unit": "lux"
},
"sources": [
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "lux",
"tags": {
"device": [],
"group": []
}
},
"color": "#f39c12",
"name": "Light",
"source": "bucket",
"timespan": {
"mode": "latest"
}
}
],
"type": "donutchart"
},
{
"layout": {
"col": 2,
"row": 6,
"sizeX": 2,
"sizeY": 6
},
"panel": {
"color": "#ffffff",
"currentColor": "#ffffff",
"showOffline": {
"type": "none"
},
"title": "Noise Level (Avg)"
},
"properties": {
"color": "#9933ff",
"max": 100,
"min": 0,
"unit": "dBA"
},
"sources": [
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "noiseAvg",
"tags": {
"device": [],
"group": []
}
},
"color": "#9b59b6",
"name": "Noise Avg",
"source": "bucket",
"timespan": {
"mode": "latest"
}
}
],
"type": "donutchart"
},
{
"layout": {
"col": 0,
"row": 12,
"sizeX": 2,
"sizeY": 6
},
"panel": {
"color": "#ffffff",
"currentColor": "#ffffff",
"showOffline": {
"type": "none"
},
"title": "CO2"
},
"properties": {
"color": "#00aa00",
"max": 2000,
"min": 400,
"unit": "ppm"
},
"sources": [
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "co2",
"tags": {
"device": [],
"group": []
}
},
"color": "#27ae60",
"name": "CO2",
"source": "bucket",
"timespan": {
"mode": "latest"
}
}
],
"type": "donutchart"
},
{
"layout": {
"col": 2,
"row": 12,
"sizeX": 2,
"sizeY": 6
},
"panel": {
"color": "#ffffff",
"currentColor": "#ffffff",
"showOffline": {
"type": "none"
},
"title": "TVOC"
},
"properties": {
"color": "#ff6600",
"max": 1000,
"min": 0,
"unit": "ppb"
},
"sources": [
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "tvoc",
"tags": {
"device": [],
"group": []
}
},
"color": "#e67e22",
"name": "TVOC",
"source": "bucket",
"timespan": {
"mode": "latest"
}
}
],
"type": "donutchart"
},
{
"layout": {
"col": 4,
"row": 0,
"sizeX": 2,
"sizeY": 12
},
"panel": {
"color": "#ffffff",
"currentColor": "#ffffff",
"showOffline": {
"type": "none"
},
"title": "Temperature & Humidity History"
},
"properties": {
"axis": true,
"fill": false,
"legend": true,
"multiple_axes": true
},
"sources": [
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "temperature",
"tags": {
"device": [],
"group": []
}
},
"color": "#e74c3c",
"name": "Temperature",
"source": "bucket",
"timespan": {
"magnitude": "hour",
"mode": "relative",
"period": "latest",
"value": 24
}
},
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "hygrometry",
"tags": {
"device": [],
"group": []
}
},
"color": "#3498db",
"name": "Humidity",
"source": "bucket",
"timespan": {
"magnitude": "hour",
"mode": "relative",
"period": "latest",
"value": 24
}
}
],
"type": "chart"
},
{
"layout": {
"col": 4,
"row": 12,
"sizeX": 2,
"sizeY": 6
},
"panel": {
"color": "#ffffff",
"currentColor": "#ffffff",
"showOffline": {
"type": "none"
},
"title": "Air Quality History"
},
"properties": {
"axis": true,
"fill": false,
"legend": true,
"multiple_axes": true
},
"sources": [
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "co2",
"tags": {
"device": [],
"group": []
}
},
"color": "#27ae60",
"name": "CO2",
"source": "bucket",
"timespan": {
"magnitude": "hour",
"mode": "relative",
"period": "latest",
"value": 24
}
},
{
"bucket": {
"backend": "mongodb",
"id": "greenme_cube_data",
"mapping": "tvoc",
"tags": {
"device": [],
"group": []
}
},
"color": "#e67e22",
"name": "TVOC",
"source": "bucket",
"timespan": {
"magnitude": "hour",
"mode": "relative",
"period": "latest",
"value": 24
}
}
],
"type": "chart"
}
]
}
]
}
}
]
}
}
]
}
}