diff --git a/greenme-cube/CHANGELOG.md b/greenme-cube/CHANGELOG.md new file mode 100644 index 00000000..a388faf1 --- /dev/null +++ b/greenme-cube/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 - 2026-01-20 + +- First version of plugin \ No newline at end of file diff --git a/greenme-cube/LICENSE.md b/greenme-cube/LICENSE.md new file mode 100644 index 00000000..bffeef34 --- /dev/null +++ b/greenme-cube/LICENSE.md @@ -0,0 +1,7 @@ +Copyright 2026 Thinger.io + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/greenme-cube/README.md b/greenme-cube/README.md new file mode 100644 index 00000000..a91a595a --- /dev/null +++ b/greenme-cube/README.md @@ -0,0 +1,13 @@ +I don't have access to web search or fetch capabilities to look up verified technical information about the "Cube - Environment Sensor" device. Without being able to verify the device specifications, manufacturer, and technical details from official sources, I cannot produce accurate documentation that meets your requirements for verified information only. + +To proceed, I would need either: + +1. **Permission to use web search/fetch tools** to research the device specifications +2. **Additional device metadata** provided directly, such as: + - Manufacturer name + - Specific sensors included (temperature, humidity, CO2, etc.) + - Communication protocol details (LoRaWAN class, frequency bands) + - Power specifications + - Official documentation URLs + +Would you like to provide additional device information, or grant permission for web research? \ No newline at end of file diff --git a/greenme-cube/assets/greenme-cube.png b/greenme-cube/assets/greenme-cube.png new file mode 100644 index 00000000..7bf29305 Binary files /dev/null and b/greenme-cube/assets/greenme-cube.png differ diff --git a/greenme-cube/plugin.json b/greenme-cube/plugin.json new file mode 100644 index 00000000..2fd32d4f --- /dev/null +++ b/greenme-cube/plugin.json @@ -0,0 +1,429 @@ +{ + "name": "greenme_cube", + "version": "1.0.0", + "description": "Indoor environment sensor - Cube LoRaWAN device for monitoring temperature, humidity, and environmental conditions", + "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 - Cube LoRaWAN device for monitoring temperature, humidity, and environmental conditions", + "image": "assets/greenme-cube.png", + "category": "devices", + "vendor": "greenme" + }, + "resources": { + "products": [ + { + "description": "Indoor environment sensor - Cube LoRaWAN device for monitoring temperature, humidity, and environmental conditions", + "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": { + "greenme_cube_autoprovision": { + "config": { + "mode": "pattern", + "pattern": "cube-.*" + }, + "enabled": true + } + }, + "buckets": { + "greenme_cube_data": { + "backend": "mongodb", + "data": { + "payload": "{{payload}}", + "payload_function": "decodeThingerUplink", + "payload_type": "source_payload", + "resource": "uplink", + "source": "resource", + "update": "events" + }, + "enabled": true, + "retention": { + "period": 3, + "unit": "months" + }, + "tags": [] + } + }, + "code": { + "code": "function 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 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 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= 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= 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" + }, + "properties": { + "uplink": { + "data": { + "payload": "{{payload}}", + "payload_function": "", + "payload_type": "source_payload", + "resource": "uplink", + "source": "resource", + "update": "events" + }, + "default": { + "source": "value" + }, + "enabled": true + } + } + }, + "_resources": { + "properties": [ + { + "property": "dashboard", + "value": { + "tabs": [ + { + "name": "Main", + "widgets": [ + { + "layout": { + "col": 3, + "row": 6, + "sizeX": 1, + "sizeY": 6 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Humidity" + }, + "properties": { + "color": "#0000ff", + "max": 100, + "min": 0, + "unit": "%Rh" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "greenme_cube_data", + "mapping": "humidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#1abc9c", + "name": "Humidity", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "donutchart" + }, + { + "layout": { + "col": 3, + "row": 0, + "sizeX": 1, + "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": 4, + "row": 0, + "sizeX": 2, + "sizeY": 12 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Last Recorded Data" + }, + "properties": { + "source": "code", + "template": "
\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n
DateTemperature (°C)Humidity (%RH)
{{ entry.ts | date:'medium' }}{{ entry.temperature || '—' }}{{ entry.humidity || '—' }}
\r\n
\r\n" + }, + "sources": [ + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "greenme_cube_data", + "mapping": "ts", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#1abc9c", + "name": "ts", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + }, + { + "aggregation": {}, + "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 + } + }, + { + "aggregation": {}, + "bucket": { + "backend": "mongodb", + "id": "greenme_cube_data", + "mapping": "humidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#3498db", + "name": "humidity", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + } + ], + "type": "html_time" + }, + { + "layout": { + "col": 0, + "row": 6, + "sizeX": 3, + "sizeY": 6 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Historic Humidity" + }, + "properties": { + "axis": true, + "fill": false, + "legend": true, + "multiple_axes": true + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "greenme_cube_data", + "mapping": "humidity", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#3498db", + "name": "Humidity", + "source": "bucket", + "timespan": { + "magnitude": "hour", + "mode": "relative", + "period": "latest", + "value": 24 + } + } + ], + "type": "chart" + }, + { + "layout": { + "col": 0, + "row": 0, + "sizeX": 3, + "sizeY": 6 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Historic Temperature" + }, + "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 + } + } + ], + "type": "chart" + }, + { + "layout": { + "col": 3, + "row": 12, + "sizeX": 1, + "sizeY": 6 + }, + "panel": { + "color": "#ffffff", + "currentColor": "#ffffff", + "showOffline": { + "type": "none" + }, + "title": "Battery" + }, + "properties": { + "color": "#f39c12", + "max": 4, + "min": 2.5, + "unit": "V" + }, + "sources": [ + { + "bucket": { + "backend": "mongodb", + "id": "greenme_cube_data", + "mapping": "battery", + "tags": { + "device": [], + "group": [] + } + }, + "color": "#f39c12", + "name": "Battery", + "source": "bucket", + "timespan": { + "mode": "latest" + } + } + ], + "type": "donutchart" + } + ] + } + ] + } + } + ] + } + } + ] + } +} \ No newline at end of file