├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── api.html ├── api.js ├── examples └── Basic usage.json ├── icons ├── homekit-logo.png ├── icon-color.png ├── icon.png └── icon.pxd │ ├── QuickLook │ ├── Icon.tiff │ └── Thumbnail.tiff │ ├── data │ ├── 15644CBE-CB2C-4054-AAB1-064FBC3F72DB │ ├── 2D35D9CD-316A-4D5B-A1D9-1C09DB37FAC9 │ ├── BF7855C2-D304-4F39-84A3-141656F5E18C │ ├── selection │ │ ├── meta │ │ └── shapeSelection │ │ │ ├── meta │ │ │ └── path │ └── selectionForContentTransform │ │ ├── meta │ │ └── shapeSelection │ │ ├── meta │ │ └── path │ └── metadata.info ├── nodes ├── bridge.html ├── bridge.js ├── get.html ├── get.js ├── in.html ├── in.js ├── locales │ ├── en-US │ │ ├── bridge.html │ │ ├── bridge.json │ │ ├── get.html │ │ ├── get.json │ │ ├── in.html │ │ ├── in.json │ │ ├── out.html │ │ ├── out.json │ │ ├── server.html │ │ └── server.json │ └── sk-SK │ │ ├── bridge.html │ │ ├── bridge.json │ │ ├── get.html │ │ ├── get.json │ │ ├── in.html │ │ ├── in.json │ │ ├── out.html │ │ ├── out.json │ │ ├── server.html │ │ └── server.json ├── out.html ├── out.js ├── server.html └── server.js ├── package-lock.json ├── package.json ├── readme ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png └── options.gif └── resources ├── Zigbee2mqttHelper.js ├── css ├── common.css └── multiple-select.css ├── js ├── multiple-select.js ├── multiple-select.min.js └── node-red-contrib-zigbee2mqtt-helpers.js └── tokeninput ├── jquery.tokeninput.js ├── token-input-facebook.css ├── token-input-mac.css └── token-input.css /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | patreon: popov 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /node_modules/ 3 | node-red-contrib-zigbee2mqtt-*.tgz 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-red-contrib-zigbee2mqtt 2 | [![platform](https://img.shields.io/badge/platform-Node--RED-red?logo=nodered)](https://nodered.org) 3 | [![Min Node Version](https://img.shields.io/node/v/node-red-contrib-zigbee2mqtt.svg)](https://nodejs.org/en/) 4 | [![GitHub version](https://img.shields.io/github/package-json/v/andreypopov/node-red-contrib-zigbee2mqtt?logo=npm)](https://www.npmjs.com/package/node-red-contrib-zigbee2mqtt) 5 | [![GitHub stars](https://img.shields.io/github/stars/andreypopov/node-red-contrib-zigbee2mqtt)](https://github.com/andreypopov/node-red-contrib-zigbee2mqtt/stargazers) 6 | [![Package Quality](https://packagequality.com/shield/node-red-contrib-zigbee2mqtt.svg)](https://packagequality.com/#?package=node-red-contrib-zigbee2mqtt) 7 | 8 | [![issues](https://img.shields.io/github/issues/andreypopov/node-red-contrib-zigbee2mqtt?logo=github)](https://github.com/andreypopov/node-red-contrib-zigbee2mqtt/issues) 9 | ![GitHub last commit](https://img.shields.io/github/last-commit/andreypopov/node-red-contrib-zigbee2mqtt) 10 | ![NPM Total Downloads](https://img.shields.io/npm/dt/node-red-contrib-zigbee2mqtt.svg) 11 | ![NPM Downloads per month](https://img.shields.io/npm/dm/node-red-contrib-zigbee2mqtt) 12 | ![Repo size](https://img.shields.io/github/repo-size/andreypopov/node-red-contrib-zigbee2mqtt) 13 | 14 | Node-Red Nodes for Zigbee2mqtt connectivity. 15 | 16 | Available nodes are: 17 | * zigbee2mqtt-in: listen to device 18 | * zigbee2mqtt-get: get current value of device 19 | * zigbee2mqtt-out: send command to device 20 | * zigbee2mqtt-bridge: logs, options, other events 21 | 22 | Extra features: 23 | * groups support 24 | * network map generation 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | # Support 33 | Developing and supporting this plugin needs time and efforts. Appreciate your support on [Patreon](https://www.patreon.com/bePatron?u=12661781). Here, you can sign up to be a member and help support my project. 34 | -------------------------------------------------------------------------------- /api.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | var NODE_PATH = '/zigbee2mqtt/'; 2 | 3 | module.exports = function(RED) { 4 | RED.httpAdmin.get(NODE_PATH + 'getDevices', function (req, res) { 5 | var config = req.query; 6 | var controller = RED.nodes.getNode(config.controllerID); 7 | 8 | if (controller && controller.constructor.name === "ServerNode") { 9 | controller.getDevices(function (items) { 10 | if (items) { 11 | res.json(items); 12 | } else { 13 | res.status(404).end(); 14 | } 15 | }, true); 16 | } else { 17 | res.json([{},{}]); 18 | } 19 | }); 20 | 21 | RED.httpAdmin.get(NODE_PATH + 'restart', function (req, res) { 22 | var config = req.query; 23 | var controller = RED.nodes.getNode(config.controllerID); 24 | if (controller && controller.constructor.name === "ServerNode") { 25 | controller.restart(); 26 | res.json({"result":"ok"}); 27 | } else { 28 | res.status(404).end(); 29 | } 30 | }); 31 | 32 | RED.httpAdmin.get(NODE_PATH + 'setPermitJoin', function (req, res) { 33 | var config = req.query; 34 | var controller = RED.nodes.getNode(config.controllerID); 35 | if (controller && controller.constructor.name === "ServerNode") { 36 | controller.setPermitJoin(config.permit_join==='true'?true:false); 37 | res.json({"result":"ok"}); 38 | } else { 39 | res.status(404).end(); 40 | } 41 | }); 42 | 43 | RED.httpAdmin.get(NODE_PATH + 'setLogLevel', function (req, res) { 44 | var config = req.query; 45 | var controller = RED.nodes.getNode(config.controllerID); 46 | if (controller && controller.constructor.name === "ServerNode") { 47 | controller.setLogLevel(config.log_level); 48 | res.json({"result":"ok"}); 49 | } else { 50 | res.status(404).end(); 51 | } 52 | }); 53 | 54 | RED.httpAdmin.get(NODE_PATH + 'getConfig', function (req, res) { 55 | var config = req.query; 56 | var controller = RED.nodes.getNode(config.controllerID); 57 | if (controller && controller.constructor.name === "ServerNode") { 58 | res.json(controller.bridge_info); 59 | } else { 60 | res.status(404).end(); 61 | } 62 | }); 63 | 64 | RED.httpAdmin.get(NODE_PATH + 'renameDevice', function (req, res) { 65 | var config = req.query; 66 | var controller = RED.nodes.getNode(config.controllerID); 67 | if (controller && controller.constructor.name === "ServerNode") { 68 | var response = controller.renameDevice(config.ieee_address, config.newName); 69 | res.json(response); 70 | } else { 71 | res.status(404).end(); 72 | } 73 | }); 74 | 75 | RED.httpAdmin.get(NODE_PATH + 'removeDevice', function (req, res) { 76 | var config = req.query; 77 | var controller = RED.nodes.getNode(config.controllerID); 78 | if (controller && controller.constructor.name === "ServerNode") { 79 | var response = controller.removeDevice(config.id, config.newName); 80 | res.json(response); 81 | } else { 82 | res.status(404).end(); 83 | } 84 | }); 85 | 86 | RED.httpAdmin.get(NODE_PATH + 'renameGroup', function (req, res) { 87 | var config = req.query; 88 | var controller = RED.nodes.getNode(config.controllerID); 89 | if (controller && controller.constructor.name === "ServerNode") { 90 | var response = controller.renameGroup(config.id, config.newName); 91 | res.json(response); 92 | } else { 93 | res.status(404).end(); 94 | } 95 | }); 96 | 97 | RED.httpAdmin.get(NODE_PATH + 'removeGroup', function (req, res) { 98 | var config = req.query; 99 | var controller = RED.nodes.getNode(config.controllerID); 100 | if (controller && controller.constructor.name === "ServerNode") { 101 | var response = controller.removeGroup(config.id); 102 | res.json(response); 103 | } else { 104 | res.status(404).end(); 105 | } 106 | }); 107 | 108 | RED.httpAdmin.get(NODE_PATH + 'addGroup', function (req, res) { 109 | var config = req.query; 110 | var controller = RED.nodes.getNode(config.controllerID); 111 | if (controller && controller.constructor.name === "ServerNode") { 112 | var response = controller.addGroup(config.name); 113 | res.json(response); 114 | } else { 115 | res.status(404).end(); 116 | } 117 | }); 118 | 119 | RED.httpAdmin.get(NODE_PATH + 'removeDeviceFromGroup', function (req, res) { 120 | var config = req.query; 121 | var controller = RED.nodes.getNode(config.controllerID); 122 | if (controller && controller.constructor.name === "ServerNode") { 123 | var response = controller.removeDeviceFromGroup(config.deviceId, config.groupId); 124 | res.json(response); 125 | } else { 126 | res.status(404).end(); 127 | } 128 | }); 129 | 130 | RED.httpAdmin.get(NODE_PATH + 'addDeviceToGroup', function (req, res) { 131 | var config = req.query; 132 | var controller = RED.nodes.getNode(config.controllerID); 133 | if (controller && controller.constructor.name === "ServerNode") { 134 | var response = controller.addDeviceToGroup(config.deviceId, config.groupId); 135 | res.json(response); 136 | } else { 137 | res.status(404).end(); 138 | } 139 | }); 140 | 141 | RED.httpAdmin.get(NODE_PATH + 'refreshMap', function (req, res) { 142 | var config = req.query; 143 | var controller = RED.nodes.getNode(config.controllerID); 144 | if (controller && controller.constructor.name === "ServerNode") { 145 | controller.refreshMap(true, config.engine).then(function(response){ 146 | res.json(response); 147 | }).catch(error => { 148 | res.status(404).end(); 149 | }); 150 | } else { 151 | res.status(404).end(); 152 | } 153 | }); 154 | RED.httpAdmin.get(NODE_PATH + 'showMap', function (req, res) { 155 | var config = req.query; 156 | var controller = RED.nodes.getNode(config.controllerID); 157 | if (controller && controller.constructor.name === "ServerNode") { 158 | var response = controller.map; 159 | res.writeHead(200, {'Content-Type': 'image/svg+xml'}); 160 | res.end(response); // Send the file data to the browser. 161 | } else { 162 | res.status(404).end(); 163 | } 164 | }); 165 | } 166 | -------------------------------------------------------------------------------- /examples/Basic usage.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "123545dd506fb882", 4 | "type": "zigbee2mqtt-in", 5 | "z": "d9c248a1ac73f01a", 6 | "name": "", 7 | "server": "183e467f17e7451d", 8 | "friendly_name": "Outlet 1", 9 | "device_id": "0xa4c13880d0cef5c9", 10 | "state": "state", 11 | "outputAtStartup": true, 12 | "x": 930, 13 | "y": 560, 14 | "wires": [ 15 | [ 16 | "1faf0be4d4395a25" 17 | ] 18 | ] 19 | }, 20 | { 21 | "id": "4834aa1faa625685", 22 | "type": "zigbee2mqtt-bridge", 23 | "z": "d9c248a1ac73f01a", 24 | "name": "", 25 | "server": "183e467f17e7451d", 26 | "topic": null, 27 | "x": 980, 28 | "y": 160, 29 | "wires": [ 30 | [] 31 | ] 32 | }, 33 | { 34 | "id": "b38b944ffd4e9a66", 35 | "type": "zigbee2mqtt-out", 36 | "z": "d9c248a1ac73f01a", 37 | "name": "", 38 | "server": "183e467f17e7451d", 39 | "friendly_name": "0x9035eafffe6acbed", 40 | "device_id": "0x9035eafffe6acbed", 41 | "command": "position", 42 | "commandType": "z2m_cmd", 43 | "payload": "payload", 44 | "payloadType": "msg", 45 | "transition": 0, 46 | "x": 1460, 47 | "y": 460, 48 | "wires": [] 49 | }, 50 | { 51 | "id": "33e8ef8026f7ecc1", 52 | "type": "inject", 53 | "z": "d9c248a1ac73f01a", 54 | "name": "", 55 | "props": [ 56 | { 57 | "p": "payload" 58 | }, 59 | { 60 | "p": "topic", 61 | "vt": "str" 62 | } 63 | ], 64 | "repeat": "", 65 | "crontab": "", 66 | "once": false, 67 | "onceDelay": 0.1, 68 | "topic": "", 69 | "payload": "10", 70 | "payloadType": "num", 71 | "x": 1250, 72 | "y": 460, 73 | "wires": [ 74 | [ 75 | "b38b944ffd4e9a66" 76 | ] 77 | ] 78 | }, 79 | { 80 | "id": "62a13be50b93dcc9", 81 | "type": "inject", 82 | "z": "d9c248a1ac73f01a", 83 | "name": "", 84 | "props": [ 85 | { 86 | "p": "payload" 87 | }, 88 | { 89 | "p": "topic", 90 | "vt": "str" 91 | } 92 | ], 93 | "repeat": "", 94 | "crontab": "", 95 | "once": false, 96 | "onceDelay": 0.1, 97 | "topic": "", 98 | "payload": "20", 99 | "payloadType": "num", 100 | "x": 1250, 101 | "y": 500, 102 | "wires": [ 103 | [ 104 | "b38b944ffd4e9a66" 105 | ] 106 | ] 107 | }, 108 | { 109 | "id": "1faf0be4d4395a25", 110 | "type": "debug", 111 | "z": "d9c248a1ac73f01a", 112 | "name": "", 113 | "active": true, 114 | "tosidebar": true, 115 | "console": false, 116 | "tostatus": true, 117 | "complete": "true", 118 | "targetType": "full", 119 | "statusVal": "payload", 120 | "statusType": "auto", 121 | "x": 1150, 122 | "y": 560, 123 | "wires": [] 124 | }, 125 | { 126 | "id": "89edc10dcce1d277", 127 | "type": "inject", 128 | "z": "d9c248a1ac73f01a", 129 | "name": "test", 130 | "props": [ 131 | { 132 | "p": "payload" 133 | }, 134 | { 135 | "p": "topic", 136 | "vt": "str" 137 | } 138 | ], 139 | "repeat": "", 140 | "crontab": "", 141 | "once": false, 142 | "onceDelay": 0.1, 143 | "topic": "test", 144 | "payload": "", 145 | "payloadType": "date", 146 | "x": 950, 147 | "y": 360, 148 | "wires": [ 149 | [ 150 | "7fdcec472ec527db" 151 | ] 152 | ] 153 | }, 154 | { 155 | "id": "7fdcec472ec527db", 156 | "type": "zigbee2mqtt-get", 157 | "z": "d9c248a1ac73f01a", 158 | "name": "", 159 | "server": "183e467f17e7451d", 160 | "friendly_name": "", 161 | "device_id": "", 162 | "state": "0", 163 | "x": 1240, 164 | "y": 380, 165 | "wires": [ 166 | [ 167 | "8b9d391ced3e6d4c" 168 | ] 169 | ] 170 | }, 171 | { 172 | "id": "8b9d391ced3e6d4c", 173 | "type": "debug", 174 | "z": "d9c248a1ac73f01a", 175 | "name": "", 176 | "active": true, 177 | "tosidebar": true, 178 | "console": false, 179 | "tostatus": false, 180 | "complete": "true", 181 | "targetType": "full", 182 | "statusVal": "", 183 | "statusType": "auto", 184 | "x": 1430, 185 | "y": 380, 186 | "wires": [] 187 | }, 188 | { 189 | "id": "01f67f868cbc9d5b", 190 | "type": "inject", 191 | "z": "d9c248a1ac73f01a", 192 | "name": "Outlet 1", 193 | "props": [ 194 | { 195 | "p": "payload" 196 | }, 197 | { 198 | "p": "topic", 199 | "vt": "str" 200 | } 201 | ], 202 | "repeat": "", 203 | "crontab": "", 204 | "once": false, 205 | "onceDelay": 0.1, 206 | "topic": "Outlet 1", 207 | "payload": "", 208 | "payloadType": "date", 209 | "x": 950, 210 | "y": 400, 211 | "wires": [ 212 | [ 213 | "7fdcec472ec527db" 214 | ] 215 | ] 216 | }, 217 | { 218 | "id": "f79972418404f0bb", 219 | "type": "inject", 220 | "z": "d9c248a1ac73f01a", 221 | "name": "zigbee2mqtt_Outlet1", 222 | "props": [ 223 | { 224 | "p": "payload" 225 | }, 226 | { 227 | "p": "topic", 228 | "vt": "str" 229 | } 230 | ], 231 | "repeat": "", 232 | "crontab": "", 233 | "once": false, 234 | "onceDelay": 0.1, 235 | "topic": "zigbee2mqtt_Outlet1", 236 | "payload": "", 237 | "payloadType": "date", 238 | "x": 1000, 239 | "y": 440, 240 | "wires": [ 241 | [ 242 | "7fdcec472ec527db" 243 | ] 244 | ] 245 | }, 246 | { 247 | "id": "6cd3c363497388dc", 248 | "type": "inject", 249 | "z": "d9c248a1ac73f01a", 250 | "name": "zigbee2mqtt/Outlet 1", 251 | "props": [ 252 | { 253 | "p": "payload" 254 | }, 255 | { 256 | "p": "topic", 257 | "vt": "str" 258 | } 259 | ], 260 | "repeat": "", 261 | "crontab": "", 262 | "once": false, 263 | "onceDelay": 0.1, 264 | "topic": "zigbee2mqtt/Outlet 1", 265 | "payload": "", 266 | "payloadType": "date", 267 | "x": 1000, 268 | "y": 320, 269 | "wires": [ 270 | [ 271 | "7fdcec472ec527db" 272 | ] 273 | ] 274 | }, 275 | { 276 | "id": "3cb917c78bc66b0e", 277 | "type": "inject", 278 | "z": "d9c248a1ac73f01a", 279 | "name": "", 280 | "props": [ 281 | { 282 | "p": "payload" 283 | }, 284 | { 285 | "p": "topic", 286 | "vt": "str" 287 | } 288 | ], 289 | "repeat": "", 290 | "crontab": "", 291 | "once": false, 292 | "onceDelay": 0.1, 293 | "topic": "no device", 294 | "payload": "", 295 | "payloadType": "date", 296 | "x": 1010, 297 | "y": 220, 298 | "wires": [ 299 | [ 300 | "7fdcec472ec527db" 301 | ] 302 | ] 303 | }, 304 | { 305 | "id": "183e467f17e7451d", 306 | "type": "zigbee2mqtt-server", 307 | "name": "", 308 | "host": "192.168.1.2", 309 | "mqtt_port": "1883", 310 | "mqtt_username": "", 311 | "mqtt_password": "", 312 | "tls": "", 313 | "usetls": false, 314 | "base_topic": "zigbee2mqtt" 315 | } 316 | ] -------------------------------------------------------------------------------- /icons/homekit-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/homekit-logo.png -------------------------------------------------------------------------------- /icons/icon-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon-color.png -------------------------------------------------------------------------------- /icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.png -------------------------------------------------------------------------------- /icons/icon.pxd/QuickLook/Icon.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.pxd/QuickLook/Icon.tiff -------------------------------------------------------------------------------- /icons/icon.pxd/QuickLook/Thumbnail.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.pxd/QuickLook/Thumbnail.tiff -------------------------------------------------------------------------------- /icons/icon.pxd/data/15644CBE-CB2C-4054-AAB1-064FBC3F72DB: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.pxd/data/15644CBE-CB2C-4054-AAB1-064FBC3F72DB -------------------------------------------------------------------------------- /icons/icon.pxd/data/2D35D9CD-316A-4D5B-A1D9-1C09DB37FAC9: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.pxd/data/2D35D9CD-316A-4D5B-A1D9-1C09DB37FAC9 -------------------------------------------------------------------------------- /icons/icon.pxd/data/BF7855C2-D304-4F39-84A3-141656F5E18C: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.pxd/data/BF7855C2-D304-4F39-84A3-141656F5E18C -------------------------------------------------------------------------------- /icons/icon.pxd/data/selection/meta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | backingScale 6 | 1 7 | mode 8 | 0 9 | shapeSelectionFilename 10 | shapeSelection 11 | size 12 | 13 | NC10UHpTVFAQAAAAQGbAAAAAAABAZ2AAAAAAAA== 14 | 15 | softness 16 | 0.0 17 | timestamp 18 | 606977233.632658 19 | transform 20 | 21 | 1.4945054945054939 22 | 0.0 23 | 0.0 24 | 1.5454545454545454 25 | -50.956043956043892 26 | -117.27272727272721 27 | 0.0 28 | 0.0 29 | 30 | version 31 | 2 32 | 33 | 34 | -------------------------------------------------------------------------------- /icons/icon.pxd/data/selection/shapeSelection/meta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | backingScale 6 | 1 7 | pathFilename 8 | path 9 | version 10 | 1 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/icon.pxd/data/selection/shapeSelection/path: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.pxd/data/selection/shapeSelection/path -------------------------------------------------------------------------------- /icons/icon.pxd/data/selectionForContentTransform/meta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | backingScale 6 | 1 7 | mode 8 | 0 9 | shapeSelectionFilename 10 | shapeSelection 11 | size 12 | 13 | NC10UHpTVFAQAAAAQGbAAAAAAABAZ2AAAAAAAA== 14 | 15 | softness 16 | 0.0 17 | timestamp 18 | 606977182.81130302 19 | transform 20 | 21 | 1 22 | 0.0 23 | 0.0 24 | 1 25 | 0.0 26 | 0.0 27 | 0.0 28 | 0.0 29 | 30 | version 31 | 2 32 | 33 | 34 | -------------------------------------------------------------------------------- /icons/icon.pxd/data/selectionForContentTransform/shapeSelection/meta: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | backingScale 6 | 1 7 | pathFilename 8 | path 9 | version 10 | 1 11 | 12 | 13 | -------------------------------------------------------------------------------- /icons/icon.pxd/data/selectionForContentTransform/shapeSelection/path: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.pxd/data/selectionForContentTransform/shapeSelection/path -------------------------------------------------------------------------------- /icons/icon.pxd/metadata.info: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/icons/icon.pxd/metadata.info -------------------------------------------------------------------------------- /nodes/bridge.html: -------------------------------------------------------------------------------- 1 | 17 | 20 | 21 | 22 | 23 | 151 | 152 | 523 | 524 | -------------------------------------------------------------------------------- /nodes/bridge.js: -------------------------------------------------------------------------------- 1 | const Zigbee2mqttHelper = require('../resources/Zigbee2mqttHelper.js'); 2 | 3 | module.exports = function(RED) { 4 | 5 | class Zigbee2mqttNodeBridge { 6 | constructor(config) { 7 | 8 | RED.nodes.createNode(this, config); 9 | 10 | let node = this; 11 | node.config = config; 12 | node.cleanTimer = null; 13 | node.is_subscribed = false; 14 | node.server = RED.nodes.getNode(node.config.server); 15 | 16 | if (node.server) { 17 | node.listener_onMQTTConnect = function() { node.onMQTTConnect(); } 18 | node.server.on('onMQTTConnect', node.listener_onMQTTConnect); 19 | 20 | node.listener_onMQTTMessageBridge = function(data) { node.onMQTTMessageBridge(data); } 21 | node.server.on('onMQTTMessageBridge', node.listener_onMQTTMessageBridge); 22 | 23 | node.on('close', () => node.onClose()); 24 | 25 | if (typeof(node.server.mqtt) === 'object') { 26 | node.onMQTTConnect(); 27 | } 28 | } else { 29 | node.status({ 30 | fill: "red", 31 | shape: "dot", 32 | text: "node-red-contrib-zigbee2mqtt/bridge:status.no_server" 33 | }); 34 | } 35 | 36 | if (node.server) { 37 | node.on('input', function (message_in) { 38 | node.log('Published to mqtt topic: ' + message_in.topic + ' Payload: ' + JSON.stringify(message_in.payload)); 39 | node.server.mqtt.publish(message_in.topic, JSON.stringify(message_in.payload)); 40 | }); 41 | 42 | } else { 43 | node.status({ 44 | fill: "red", 45 | shape: "dot", 46 | text: "node-red-contrib-zigbee2mqtt/bridge:status.no_server" 47 | }); 48 | } 49 | } 50 | 51 | onClose() { 52 | let node = this; 53 | node.setNodeStatus(); 54 | 55 | //remove listeners 56 | if (node.listener_onMQTTConnect) { 57 | node.server.removeListener('onMQTTConnect', node.listener_onMQTTConnect); 58 | } 59 | if (node.listener_onMQTTMessageBridge) { 60 | node.server.removeListener("onMQTTMessageBridge", node.listener_onMQTTMessageBridge); 61 | } 62 | } 63 | 64 | onMQTTConnect() { 65 | let node = this; 66 | node.setNodeStatus(); 67 | } 68 | 69 | setNodeStatus() { 70 | let node = this; 71 | 72 | if (node.server.bridge_info && node.server.bridge_info.permit_join && node.server.bridge_state) { 73 | node.status({ 74 | fill: "yellow", 75 | shape: "ring", 76 | text: "node-red-contrib-zigbee2mqtt/bridge:status.searching" 77 | }); 78 | } else { 79 | let text = node.server.bridge_state?RED._("node-red-contrib-zigbee2mqtt/bridge:status.online"):RED._("node-red-contrib-zigbee2mqtt/bridge:status.offline"); 80 | if (node.server.bridge_info && "log_level" in node.server.bridge_info) { 81 | text += ' (log: '+node.server.bridge_info.log_level+')'; 82 | } 83 | node.status({ 84 | fill: node.server.bridge_state?"green":"red", 85 | shape: "dot", 86 | text: text 87 | }); 88 | } 89 | } 90 | 91 | onMQTTMessageBridge(data) { 92 | let node = this; 93 | let payload = Zigbee2mqttHelper.isJson(data.payload)?JSON.parse(data.payload):data.payload; 94 | 95 | if (node.server.getTopic('/bridge/state') === data.topic) { 96 | node.setNodeStatus(); 97 | } else if (node.server.getTopic('/bridge/info') === data.topic) { 98 | if (payload.permit_join != (node.status.fill === 'yellow')) { 99 | node.setNodeStatus(); 100 | } 101 | } else if (node.server.getTopic('/bridge/event') === data.topic) { 102 | node.status({ 103 | fill: "yellow", 104 | shape: "ring", 105 | text: payload.type 106 | }); 107 | clearTimeout(node.cleanTimer); 108 | node.cleanTimer = setTimeout(function(){ 109 | node.setNodeStatus(); 110 | }, 10000); 111 | } 112 | 113 | node.send({ 114 | payload: payload, 115 | topic: data.topic 116 | }); 117 | } 118 | } 119 | RED.nodes.registerType('zigbee2mqtt-bridge', Zigbee2mqttNodeBridge); 120 | }; 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /nodes/get.html: -------------------------------------------------------------------------------- 1 | 37 | 38 | 102 | 103 | -------------------------------------------------------------------------------- /nodes/get.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | class Zigbee2mqttNodeGet { 3 | constructor(config) { 4 | RED.nodes.createNode(this, config); 5 | 6 | let node = this; 7 | node.config = config; 8 | node.cleanTimer = null; 9 | node.last_successful_status = {}; 10 | node.server = RED.nodes.getNode(node.config.server); 11 | node.status({}); 12 | if (node.server) { 13 | node.on('input', function(message_in) { 14 | 15 | let key = node.config.device_id; 16 | if ((!key || key === 'msg.topic') && message_in.topic) { 17 | key = message_in.topic; 18 | } 19 | 20 | node.server.nodeSend(node, { 21 | 'msg': message_in, 22 | 'key': key, 23 | }); 24 | }); 25 | 26 | } else { 27 | node.status({ 28 | fill: 'red', 29 | shape: 'dot', 30 | text: 'node-red-contrib-zigbee2mqtt/server:status.no_server', 31 | }); 32 | } 33 | } 34 | 35 | setSuccessfulStatus(obj) { 36 | this.status(obj); 37 | this.last_successful_status = obj; 38 | } 39 | } 40 | 41 | RED.nodes.registerType('zigbee2mqtt-get', Zigbee2mqttNodeGet); 42 | }; 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /nodes/in.html: -------------------------------------------------------------------------------- 1 | 42 | 43 | 126 | -------------------------------------------------------------------------------- /nodes/in.js: -------------------------------------------------------------------------------- 1 | module.exports = function(RED) { 2 | class Zigbee2mqttNodeIn { 3 | constructor(config) { 4 | RED.nodes.createNode(this, config); 5 | 6 | let node = this; 7 | node.config = config; 8 | node.firstMsg = true; 9 | node.cleanTimer = null; 10 | node.server = RED.nodes.getNode(node.config.server); 11 | node.last_value = null; 12 | node.last_successful_status = {}; 13 | node.status({}); 14 | 15 | if (node.server) { 16 | node.listener_onMQTTAvailability = function(data) { node.onMQTTAvailability(data); } 17 | node.server.on('onMQTTAvailability', node.listener_onMQTTAvailability); 18 | 19 | node.listener_onConnectError = function(data) { node.onConnectError(); } 20 | node.server.on('onConnectError', node.listener_onConnectError); 21 | 22 | node.listener_onMQTTMessage = function(data) { node.onMQTTMessage(data); } 23 | node.server.on('onMQTTMessage', node.listener_onMQTTMessage); 24 | 25 | node.listener_onMQTTBridgeState = function(data) { node.onMQTTBridgeState(data); } 26 | node.server.on('onMQTTBridgeState', node.listener_onMQTTBridgeState); 27 | 28 | node.on('close', () => node.onClose()); 29 | 30 | } else { 31 | node.status({ 32 | fill: "red", 33 | shape: "dot", 34 | text: "node-red-contrib-zigbee2mqtt/in:status.no_server" 35 | }); 36 | } 37 | } 38 | 39 | onMQTTAvailability(data) { 40 | let node = this; 41 | 42 | if (data.item && 'ieee_address' in data.item && data.item.ieee_address === node.config.device_id) { 43 | node.server.nodeSend(node, { 44 | 'node_send': false 45 | }); 46 | } 47 | } 48 | 49 | onMQTTMessage(data) { 50 | let node = this; 51 | 52 | if (node.config.enableMultiple) { 53 | if (data.item && 54 | (("ieee_address" in data.item && (node.config.device_id).includes(data.item.ieee_address)) 55 | || ("id" in data.item && (node.config.device_id).includes(data.item.id))) 56 | ) { 57 | node.server.nodeSend(node, { 58 | 'changed' : data 59 | }); 60 | } 61 | 62 | 63 | } else { 64 | if (data.item && 65 | (("ieee_address" in data.item && data.item.ieee_address === node.config.device_id) 66 | || ("id" in data.item && parseInt(data.item.id) === parseInt(node.config.device_id))) 67 | ) { 68 | node.server.nodeSend(node, { 69 | 'filter': node.config.filterChanges 70 | }); 71 | } 72 | } 73 | 74 | } 75 | 76 | onMQTTBridgeState(data) { 77 | let node = this; 78 | if (data.payload) { 79 | node.status(node.last_successful_status); 80 | } else { 81 | node.onConnectError(); 82 | } 83 | } 84 | 85 | onConnectError() { 86 | this.status({ 87 | fill: "red", 88 | shape: "dot", 89 | text: "node-red-contrib-zigbee2mqtt/in:status.no_connection" 90 | }); 91 | } 92 | 93 | 94 | onClose() { 95 | let node = this; 96 | 97 | if (node.listener_onMQTTAvailability) { 98 | node.server.removeListener("onMQTTAvailability", node.listener_onMQTTAvailability); 99 | } 100 | if (node.listener_onConnectError) { 101 | node.server.removeListener("onConnectError", node.listener_onConnectError); 102 | } 103 | if (node.listener_onMQTTMessage) { 104 | node.server.removeListener("onMQTTMessage", node.listener_onMQTTMessage); 105 | } 106 | if (node.listener_onMQTTBridgeState) { 107 | node.server.removeListener("onMQTTBridgeState", node.listener_onMQTTBridgeState); 108 | } 109 | 110 | node.onConnectError(); 111 | } 112 | 113 | setSuccessfulStatus(obj) { 114 | this.status(obj); 115 | this.last_successful_status = obj; 116 | } 117 | 118 | } 119 | RED.nodes.registerType('zigbee2mqtt-in', Zigbee2mqttNodeIn); 120 | }; 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /nodes/locales/en-US/bridge.html: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /nodes/locales/en-US/bridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "name": "Name", 4 | "server": "Server", 5 | "permit_join": "Permit Join", 6 | "permit_join_help": "Start searching devices for 3 minutes", 7 | "permit_join_cancel_help": "Stop search", 8 | "log_level": "Log Level", 9 | "log_level_info": "info", 10 | "log_level_debug": "debug", 11 | "log_level_warning": "warning", 12 | "log_level_error": "error", 13 | "version": "Version", 14 | "restart_required": "Restart required", 15 | "no_legacy_api": "No Legacy API", 16 | "coordinator": "Coordinator", 17 | "turn_off_legacy_api": "Should be off", 18 | "restart_needed": "Restart needed", 19 | "ok": "OK", 20 | "advanced_output": "Output json", 21 | "should_be_json": "Should be 'json'", 22 | "restart_zigbee2mqtt": "Restart zigbee2mqtt", 23 | "retain_disabled_error": "force_disable_retain should be FALSE", 24 | "mqtt_disable_retain": "Retain mqtt", 25 | "restart": "Restart", 26 | "disabled": "disabled", 27 | "last_seen": "Last seen", 28 | "availability": "Availability", 29 | "sure_restart": "Are you sure?", 30 | "refresh": "Refresh", 31 | "refresh_all": "Refresh all data" 32 | }, 33 | "placeholder": { 34 | "name": "Name" 35 | }, 36 | "status": { 37 | "no_server": "no server", 38 | "no_device": "no device", 39 | "no_value": "no value", 40 | "no_connection": "no connection", 41 | "searching": "looking for devices...", 42 | "paired": "paired!", 43 | "online": "online", 44 | "offline": "offline", 45 | "pairing": "pairing...", 46 | "connected": "connected", 47 | "failed": "failed!" 48 | }, 49 | "tabs": { 50 | "bridge": "Bridge", 51 | "devices": "Devices", 52 | "groups": "Groups", 53 | "map": "Map" 54 | }, 55 | "devices": { 56 | "ieee_addr": "ieee_address", 57 | "friendly_name": "Friendly name", 58 | "model": "Model", 59 | "device": "Device", 60 | "set": "set", 61 | "remove": "remove", 62 | "sure_remove": "Are you sure you want to remove device?" 63 | }, 64 | "groups": { 65 | "id": "ID", 66 | "friendly_name": "Friendly name", 67 | "devices": "Devices", 68 | "set": "set", 69 | "remove": "remove", 70 | "add": "Add group", 71 | "enter_group_name": "Enter group name", 72 | "sure_remove": "Are you sure you want to remove group?" 73 | }, 74 | "map": { 75 | "refresh": "Refresh map", 76 | "fullscreen": "Open in full screen", 77 | "loading": "Loading..." 78 | }, 79 | "tokeninput": { 80 | "searching": "Searching...", 81 | "type_to_search": "Type to search device", 82 | "no_results": "No results" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /nodes/locales/en-US/get.html: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /nodes/locales/en-US/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "name": "Name", 4 | "server": "Server", 5 | "topic": "Device", 6 | "state": "Payload output", 7 | "refresh": "Refresh", 8 | "legacy_api": "Legacy API", 9 | "refresh_devices_list": "Refresh Devices List", 10 | "enable_multiple": "Multiple input", 11 | "enable_multiple_help": "Listen multiple devices" 12 | }, 13 | "placeholder": { 14 | "name": "Name" 15 | }, 16 | "multiselect": { 17 | "devices": "Devices", 18 | "filter_devices": "Filter devices...", 19 | "refresh": "Refresh devices list", 20 | "none_selected": "None selected", 21 | "complete_payload": "Complete state payload" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nodes/locales/en-US/in.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /nodes/locales/en-US/in.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "name": "Name", 4 | "server": "Server", 5 | "topic": "Device", 6 | "state": "Payload output", 7 | "refresh": "Refresh", 8 | "refresh_devices_list": "Refresh Devices List", 9 | "start_output": "Start output", 10 | "start_output_help": "Send value on start", 11 | "always": "Always", 12 | "on_state_change": "On state change", 13 | "on_update": "On update", 14 | "filter_changes": "Filter", 15 | "filter_changes_help": "Send only if value changes", 16 | "output": "Output", 17 | "enable_multiple": "Multiple input", 18 | "enable_multiple_help": "Listen multiple devices" 19 | }, 20 | "placeholder": { 21 | "name": "Name" 22 | }, 23 | "status": { 24 | "no_server": "no server", 25 | "no_device": "no device", 26 | "no_connection": "no connection", 27 | "connected": "connected", 28 | "received": "ok" 29 | }, 30 | "multiselect": { 31 | "devices": "Devices", 32 | "filter_devices": "Filter devices...", 33 | "refresh": "Refresh devices list", 34 | "none_selected": "None selected", 35 | "complete_payload": "Complete state payload", 36 | "zigbee2mqtt": "Zigbee2Mqtt", 37 | "homekit": "Homekit", 38 | "groups": "Groups" 39 | }, 40 | "tip": { 41 | "deploy": "Important: deploy server node to get devices list" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /nodes/locales/en-US/out.html: -------------------------------------------------------------------------------- 1 | 24 | 25 | -------------------------------------------------------------------------------- /nodes/locales/en-US/out.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "name": "Name", 4 | "server": "Server", 5 | "topic": "Device", 6 | "command": "Command", 7 | "payload": "Payload", 8 | "options": "Options", 9 | "transition": "Transition, sec", 10 | "refresh": "Refresh", 11 | "refresh_devices_list": "Refresh Devices List" 12 | }, 13 | "placeholder": { 14 | "name": "Name", 15 | "transition": "in seconds" 16 | }, 17 | "status": { 18 | "no_server": "no server", 19 | "no_device": "no device", 20 | "no_payload": "no payload" 21 | }, 22 | "help": { 23 | "important": "IMPORTANT:", 24 | "description": "Description:", 25 | "options": "Options:", 26 | "command_homekit": "This is experimental feature, works only with Lightbulb, Lock Services for now. ", 27 | "command_brightness_move": "Instead of setting a brightness by value, you can also move it and stop it after a certain time. Example: -5 payload will start moving the brightness down at 5 units per second.", 28 | "command_brightness_step": "Instead of setting a brightness by value, you can also change current value up or down.", 29 | "option_transition": "Specifies the number of seconds the transition to this state takes. Doesn't work for brightness=0, state=toggle." 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /nodes/locales/en-US/server.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /nodes/locales/en-US/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "base_topic": "Base topic", 4 | "name": "Name", 5 | "host": "MQTT Host", 6 | "mqtt_port": "MQTT Port", 7 | "mqtt_username": "MQTT Username", 8 | "mqtt_password": "MQTT Password", 9 | "use-tls": "Use TLS", 10 | "tls-config":"TLS configuration" 11 | }, 12 | "placeholder": { 13 | "name": "Name" 14 | }, 15 | "status": { 16 | "no_server": "no server", 17 | "no_device": "no device", 18 | "no_connection": "no connection", 19 | "no_value": "no value", 20 | "received": "ok" 21 | }, 22 | "tip": { 23 | "deploy": "Important: deploy server node to get devices list" 24 | }, 25 | "editor": { 26 | "groups": "Groups", 27 | "devices": "Devices", 28 | "selected": "selected", 29 | "nothing": "Nothing", 30 | "select_device": "Select device", 31 | "msg_topic": "msg.topic", 32 | "complete_payload": "Complete payload", 33 | "zigbee2mqtt": "zigbee2mqtt", 34 | "homekit": "Apple Homekit" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/bridge.html: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/bridge.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "name": "Meno", 4 | "server": "Server", 5 | "permit_join": "Povoliť pripájanie", 6 | "permit_join_help": "Začať vyhľadávanie zariadení počas 3 minút", 7 | "permit_join_cancel_help": "Zastaviť vyhľadávanie", 8 | "log_level": "Úroveň záznamu", 9 | "log_level_info": "info", 10 | "log_level_debug": "debug", 11 | "log_level_warning": "upozornenie", 12 | "log_level_error": "chyba", 13 | "version": "Verzia", 14 | "restart_required": "Vyžadovaný reštart", 15 | "no_legacy_api": "Žiadna stará API", 16 | "coordinator": "Koordinátor", 17 | "turn_off_legacy_api": "Malo by byť vypnuté", 18 | "restart_needed": "Vyžadovaný reštart", 19 | "ok": "OK", 20 | "advanced_output": "Výstup json", 21 | "should_be_json": "Malo by byť 'json'", 22 | "restart_zigbee2mqtt": "Reštartovať zigbee2mqtt", 23 | "retain_disabled_error": "force_disable_retain by malo byť FALSE", 24 | "mqtt_disable_retain": "Zachovať mqtt", 25 | "restart": "Reštart", 26 | "disabled": "vypnuté", 27 | "last_seen": "Posledný videný", 28 | "availability": "Dostupnosť", 29 | "sure_restart": "Ste si istí?", 30 | "refresh": "Obnoviť", 31 | "refresh_all": "Obnoviť všetky údaje" 32 | }, 33 | "placeholder": { 34 | "name": "Meno" 35 | }, 36 | "status": { 37 | "no_server": "žiadny server", 38 | "no_device": "žiadne zariadenie", 39 | "no_value": "žiadna hodnota", 40 | "no_connection": "žiadne pripojenie", 41 | "searching": "vyhľadávanie zariadení...", 42 | "paired": "spárované!", 43 | "online": "online", 44 | "offline": "offline", 45 | "pairing": "spárovanie...", 46 | "connected": "pripojené", 47 | "failed": "zlyhalo!" 48 | }, 49 | "tabs": { 50 | "bridge": "Most", 51 | "devices": "Zariadenia", 52 | "groups": "Skupiny", 53 | "map": "Mapa" 54 | }, 55 | "devices": { 56 | "ieee_addr": "ieee_adresa", 57 | "friendly_name": "Prijateľné meno", 58 | "model": "Model", 59 | "device": "Zariadenie", 60 | "set": "nastaviť", 61 | "remove": "odstrániť", 62 | "sure_remove": "Ste si istí, že chcete odstrániť zariadenie?" 63 | }, 64 | "groups": { 65 | "id": "ID", 66 | "friendly_name": "Prijateľné meno", 67 | "devices": "Zariadenia", 68 | "set": "nastaviť", 69 | "remove": "odstrániť", 70 | "add": "Pridať skupinu", 71 | "enter_group_name": "Zadajte názov skupiny", 72 | "sure_remove": "Ste si istí, že chcete odstrániť skupinu?" 73 | }, 74 | "map": { 75 | "refresh": "Obnoviť mapu", 76 | "fullscreen": "Otvoriť v celoobrazovkovom režime", 77 | "loading": "Načítava sa..." 78 | }, 79 | "tokeninput": { 80 | "searching": "Vyhľadávanie...", 81 | "type_to_search": "Napíšte pre vyhľadávanie zariadenia", 82 | "no_results": "Žiadne výsledky" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/get.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/get.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "name": "Meno", 4 | "server": "Server", 5 | "topic": "Zariadenie", 6 | "state": "Výstup dát", 7 | "refresh": "Obnoviť", 8 | "legacy_api": "Stará API", 9 | "refresh_devices_list": "Obnoviť zoznam zariadení" 10 | }, 11 | "placeholder": { 12 | "name": "Meno" 13 | }, 14 | "multiselect": { 15 | "devices": "Zariadenia", 16 | "filter_devices": "Filtrovať zariadenia...", 17 | "refresh": "Obnoviť zoznam zariadení", 18 | "none_selected": "Žiadne vybrané", 19 | "complete_payload": "Kompletný stavový payload" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/in.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/in.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "name": "Meno", 4 | "server": "Server", 5 | "topic": "Zariadenie", 6 | "state": "Výstup dát", 7 | "refresh": "Obnoviť", 8 | "refresh_devices_list": "Obnoviť zoznam zariadení", 9 | "start_output": "Spustiť výstup", 10 | "start_output_help": "Odoslať hodnotu pri spustení", 11 | "always": "Vždy", 12 | "on_state_change": "Pri zmene stavu", 13 | "on_update": "Pri aktualizácii", 14 | "filter_changes": "Filtrovať", 15 | "filter_changes_help": "Odoslať iba ak sa hodnota zmení", 16 | "output": "Výstup", 17 | "enable_multiple": "Povoliť viacnásobný vstup", 18 | "enable_multiple_help": "Počúvať viacero zariadení" 19 | }, 20 | "placeholder": { 21 | "name": "Meno" 22 | }, 23 | "status": { 24 | "no_server": "žiadny server", 25 | "no_device": "žiadne zariadenie", 26 | "no_connection": "žiadne pripojenie", 27 | "connected": "pripojené", 28 | "received": "OK" 29 | }, 30 | "multiselect": { 31 | "devices": "Zariadenia", 32 | "filter_devices": "Filtrovať zariadenia...", 33 | "refresh": "Obnoviť zoznam zariadení", 34 | "none_selected": "Žiadne vybrané", 35 | "complete_payload": "Kompletný stavový payload", 36 | "zigbee2mqtt": "Zigbee2Mqtt", 37 | "homekit": "Homekit", 38 | "groups": "Skupiny" 39 | }, 40 | "tip": { 41 | "deploy": "Dôležité: nainštalujte serverový uzol na získanie zoznamu zariadení" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/out.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/out.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "name": "Meno", 4 | "server": "Server", 5 | "topic": "Zariadenie", 6 | "state": "Výstup dát", 7 | "refresh": "Obnoviť", 8 | "legacy_api": "Stará API", 9 | "refresh_devices_list": "Obnoviť zoznam zariadení" 10 | }, 11 | "placeholder": { 12 | "name": "Meno" 13 | }, 14 | "multiselect": { 15 | "devices": "Zariadenia", 16 | "filter_devices": "Filtrovať zariadenia...", 17 | "refresh": "Obnoviť zoznam zariadení", 18 | "none_selected": "Žiadne vybrané", 19 | "complete_payload": "Úplné dátové výstupy" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/server.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nodes/locales/sk-SK/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "base_topic": "Základná téma", 4 | "name": "Meno", 5 | "host": "MQTT Host", 6 | "mqtt_port": "MQTT Port", 7 | "mqtt_username": "MQTT Užívateľské meno", 8 | "mqtt_password": "MQTT Heslo", 9 | "use-tls": "Použiť TLS", 10 | "tls-config": "TLS konfigurácia" 11 | }, 12 | "placeholder": { 13 | "name": "Meno" 14 | }, 15 | "status": { 16 | "no_server": "žiadny server", 17 | "no_device": "žiadne zariadenie", 18 | "no_connection": "žiadne pripojenie", 19 | "no_value": "žiadna hodnota", 20 | "received": "ok" 21 | }, 22 | "tip": { 23 | "deploy": "Dôležité: nasadiť serverový uzol na získanie zoznamu zariadení" 24 | }, 25 | "editor": { 26 | "groups": "Skupiny", 27 | "devices": "Zariadenia", 28 | "selected": "vybrané", 29 | "nothing": "Nič", 30 | "select_device": "Vybrať zariadenie", 31 | "msg_topic": "msg.téma", 32 | "complete_payload": "Kompletná záťaž", 33 | "zigbee2mqtt": "zigbee2mqtt", 34 | "homekit": "Apple Homekit" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /nodes/out.html: -------------------------------------------------------------------------------- 1 | 77 | 78 | 289 | 290 | -------------------------------------------------------------------------------- /nodes/out.js: -------------------------------------------------------------------------------- 1 | const Zigbee2mqttHelper = require('../resources/Zigbee2mqttHelper.js'); 2 | 3 | module.exports = function(RED) { 4 | class Zigbee2mqttNodeOut { 5 | constructor(config) { 6 | RED.nodes.createNode(this, config); 7 | 8 | var node = this; 9 | node.config = config; 10 | node.cleanTimer = null; 11 | node.server = RED.nodes.getNode(node.config.server); 12 | 13 | if (node.server) { 14 | node.status({}); //clean 15 | 16 | node.on('input', function(message) { 17 | clearTimeout(node.cleanTimer); 18 | 19 | let key = node.config.device_id; 20 | if ((!key || key === 'msg.topic') && message.topic) { 21 | key = message.topic; 22 | } 23 | let device = node.server.getDeviceOrGroupByKey(key); 24 | if (device) { 25 | let payload; 26 | let options = {}; 27 | switch (node.config.payloadType) { 28 | case '': 29 | case null: 30 | case 'nothing': 31 | payload = null; 32 | break; 33 | 34 | case 'flow': 35 | case 'global': { 36 | RED.util.evaluateNodeProperty(node.config.payload, node.config.payloadType, this, message, function (error, result) { 37 | if (error) { 38 | node.error(error, message); 39 | } else { 40 | payload = result; 41 | } 42 | }); 43 | break; 44 | } 45 | case 'z2m_payload': 46 | payload = node.config.payload; 47 | break; 48 | 49 | case 'num': { 50 | payload = parseInt(node.config.payload); 51 | break; 52 | } 53 | 54 | case 'str': { 55 | payload = node.config.payload; 56 | break; 57 | } 58 | 59 | case 'json': { 60 | if (Zigbee2mqttHelper.isJson(node.config.payload)) { 61 | payload = JSON.parse(node.config.payload); 62 | } else { 63 | node.warn('Incorrect payload. Waiting for valid JSON'); 64 | node.status({ 65 | fill: "red", 66 | shape: "dot", 67 | text: "node-red-contrib-zigbee2mqtt/out:status.no_payload" 68 | }); 69 | node.cleanTimer = setTimeout(function(){ 70 | node.status({}); //clean 71 | }, 3000); 72 | } 73 | break; 74 | } 75 | 76 | // case 'homekit': 77 | // payload = node.formatHomeKit(message, payload); 78 | // break; 79 | 80 | case 'msg': 81 | default: { 82 | payload = message[node.config.payload]; 83 | break; 84 | } 85 | } 86 | 87 | 88 | var command; 89 | switch (node.config.commandType) { 90 | case '': 91 | case null: 92 | case 'nothing': 93 | payload = null; 94 | break; 95 | 96 | case 'msg': { 97 | command = message[node.config.command]; 98 | break; 99 | } 100 | case 'z2m_cmd': 101 | command = node.config.command; 102 | switch (command) { 103 | case 'state': 104 | if (payload === 'toggle') { 105 | if (device.current_values && 'position' in device.current_values) { 106 | payload = device.current_values.position > 0 ? 'close' : 'open'; 107 | } 108 | } 109 | break; 110 | case 'brightness': 111 | payload = parseInt(payload); 112 | options["state"] = payload>0?"on":"Off"; 113 | break; 114 | 115 | case 'position': 116 | payload = parseInt(payload); 117 | break; 118 | 119 | case 'scene': 120 | command = 'scene_recall'; 121 | payload = parseInt(payload); 122 | break; 123 | 124 | case 'lock': 125 | command = 'state'; 126 | if (payload === 'toggle') { 127 | if (device.current_values && 'lock_state' in 128 | device.current_values && device.current_values.lock_state === 'locked') { 129 | payload = 'unlock'; 130 | } else { 131 | payload = 'lock'; 132 | } 133 | } else if (payload === 'lock' || payload == 1 || payload === true || payload === 'on') { 134 | payload = 'lock'; 135 | } else if (payload === 'unlock' || payload == 0 || payload === false || payload === 'off') { 136 | payload = 'unlock'; 137 | } 138 | break; 139 | 140 | case 'color': 141 | payload = {"color":payload}; 142 | break; 143 | case 'color_rgb': 144 | payload = {"color":{"rgb": payload}}; 145 | break; 146 | case 'color_hex': 147 | command = "color"; 148 | payload = {"color":{"hex": payload}}; 149 | break; 150 | case 'color_hsb': 151 | command = "color"; 152 | payload = {"color":{"hsb": payload}}; 153 | break; 154 | case 'color_hsv': 155 | command = "color"; 156 | payload = {"color":{"hsv": payload}}; 157 | break; 158 | case 'color_hue': 159 | command = "color"; 160 | payload = {"color":{"hue": payload}}; 161 | break; 162 | case 'color_saturation': 163 | command = "color"; 164 | payload = {"color":{"saturation": payload}}; 165 | break; 166 | 167 | case 'color_temp': 168 | 169 | break; 170 | 171 | case 'brightness_move': 172 | case 'brightness_step': 173 | case 'alert': 174 | default: { 175 | break; 176 | } 177 | } 178 | break; 179 | 180 | case 'homekit': 181 | payload = node.fromHomeKitFormat(message, device); 182 | break; 183 | 184 | case 'json': 185 | break; 186 | 187 | case 'str': 188 | default: { 189 | command = node.config.command; 190 | break; 191 | } 192 | } 193 | 194 | let optionsToSend = {}; 195 | switch (node.config.optionsType) { 196 | case '': 197 | case null: 198 | case 'nothing': 199 | break; 200 | 201 | case 'msg': 202 | if (node.config.optionsValue in message && typeof(message[node.config.optionsValue]) == 'object') { 203 | optionsToSend = message[node.config.optionsValue]; 204 | } else { 205 | node.warn('Options value has invalid format'); 206 | } 207 | break; 208 | 209 | case 'json': 210 | if (Zigbee2mqttHelper.isJson(node.config.optionsValue)) { 211 | optionsToSend = JSON.parse(node.config.optionsValue); 212 | } else { 213 | node.warn('Options value is not valid JSON, ignore: '+node.config.optionsValue); 214 | } 215 | break; 216 | 217 | default: 218 | optionsToSend[node.config.optionsType] = node.config.optionsValue; 219 | break; 220 | } 221 | 222 | //apply options 223 | if (Object.keys(optionsToSend).length) { 224 | node.server.setDeviceOptions(device.friendly_name, optionsToSend); 225 | } 226 | 227 | //empty payload, stop 228 | if (payload === null) { 229 | return false; 230 | } 231 | 232 | if (payload !== undefined) { 233 | var toSend = {}; 234 | var text = ''; 235 | if (typeof(payload) == 'object') { 236 | toSend = payload; 237 | text = 'json'; 238 | } else { 239 | toSend[command] = payload; 240 | text = command+': '+payload; 241 | } 242 | 243 | node.log('Published to mqtt topic: ' + node.server.getTopic('/'+device.friendly_name + '/set') + ' : ' + JSON.stringify(toSend)); 244 | node.server.mqtt.publish(node.server.getTopic('/'+device.friendly_name + '/set'), JSON.stringify(toSend), 245 | {'qos':parseInt(node.server.config.mqtt_qos||0)}, 246 | function(err) { 247 | if (err) { 248 | node.error(err); 249 | } 250 | }); 251 | 252 | let fill = node.server.getDeviceAvailabilityColor(node.server.getTopic('/'+device.friendly_name)); 253 | node.status({ 254 | fill: fill, 255 | shape: "dot", 256 | text: text 257 | }); 258 | let time = Zigbee2mqttHelper.statusUpdatedAt(); 259 | node.cleanTimer = setTimeout(function(){ 260 | node.status({ 261 | fill: fill, 262 | shape: "ring", 263 | text: text + ' ' + time 264 | }); 265 | }, 3000); 266 | } else { 267 | node.status({ 268 | fill: "red", 269 | shape: "dot", 270 | text: "node-red-contrib-zigbee2mqtt/out:status.no_payload" 271 | }); 272 | } 273 | } else { 274 | node.status({ 275 | fill: "red", 276 | shape: "dot", 277 | text: "node-red-contrib-zigbee2mqtt/out:status.no_device" 278 | }); 279 | } 280 | }); 281 | 282 | } else { 283 | node.status({ 284 | fill: "red", 285 | shape: "dot", 286 | text: "node-red-contrib-zigbee2mqtt/out:status.no_server" 287 | }); 288 | } 289 | } 290 | 291 | fromHomeKitFormat(message, device) { 292 | if ("hap" in message && message.hap.context === undefined) { 293 | return null; 294 | } 295 | 296 | var payload = message['payload']; 297 | var msg = {}; 298 | 299 | if (payload.On !== undefined) { 300 | if ("current_values" in device) { 301 | // if ("brightness" in device.current_values) msg['brightness'] = device.current_values.brightness; 302 | } 303 | msg['state'] = payload.On?"on":"off"; 304 | } 305 | if (payload.Brightness !== undefined) { 306 | msg['brightness'] = Zigbee2mqttHelper.convertRange(payload.Brightness, [0,100], [0,255]); 307 | device.current_values.brightness = msg['brightness']; 308 | if ("current_values" in device) { 309 | if ("current_values" in device) device.current_values.brightness = msg['brightness']; 310 | } 311 | if (payload.Brightness >= 254) payload.Brightness = 255; 312 | msg['state'] = payload.Brightness > 0?"on":"off" 313 | } 314 | if (payload.Hue !== undefined) { 315 | msg['color'] = {"hue":payload.Hue}; 316 | device.current_values.color.hue = payload.Hue; 317 | if ("current_values" in device) { 318 | if ("brightness" in device.current_values) msg['brightness'] = device.current_values.brightness; 319 | if ("color" in device.current_values && "saturation" in device.current_values.color) msg['color']['saturation'] = device.current_values.color.saturation; 320 | if ("color" in device.current_values && "hue" in device.current_values.color) device.current_values.color.hue = payload.Hue; 321 | } 322 | msg['state'] = "on"; 323 | } 324 | if (payload.Saturation !== undefined) { 325 | msg['color'] = {"saturation":payload.Saturation}; 326 | device.current_values.color.saturation = payload.Saturation; 327 | if ("current_values" in device) { 328 | if ("brightness" in device.current_values) msg['brightness'] = device.current_values.brightness; 329 | if ("color" in device.current_values && "hue" in device.current_values.color) msg['color']['hue'] = device.current_values.color.hue; 330 | if ("color" in device.current_values && "saturation" in device.current_values.color) msg['color']['saturation'] = payload.Saturation; 331 | } 332 | msg['state'] = "on"; 333 | } 334 | if (payload.ColorTemperature !== undefined) { 335 | msg['color_temp'] = Zigbee2mqttHelper.convertRange(payload.ColorTemperature, [150,500], [150,500]); 336 | device.current_values.color_temp = msg['color_temp']; 337 | if ("current_values" in device) { 338 | if ("color_temp" in device.current_values) device.current_values.color_temp = msg['color_temp']; 339 | } 340 | msg['state'] = "on"; 341 | } 342 | if (payload.LockTargetState !== undefined) { 343 | msg['state'] = payload.LockTargetState?"LOCK":"UNLOCK"; 344 | } 345 | if (payload.TargetPosition !== undefined) { 346 | msg['position'] = payload.TargetPosition; 347 | } 348 | 349 | return msg; 350 | } 351 | } 352 | 353 | 354 | RED.nodes.registerType('zigbee2mqtt-out', Zigbee2mqttNodeOut); 355 | }; 356 | 357 | 358 | 359 | 360 | 361 | 362 | -------------------------------------------------------------------------------- /nodes/server.html: -------------------------------------------------------------------------------- 1 | 47 | 48 | 96 | 97 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Andrey Popov", 4 | "email": "andrey_popov@me.com" 5 | }, 6 | "name": "node-red-contrib-zigbee2mqtt", 7 | "description": "Zigbee2mqtt connectivity nodes for node-red", 8 | "version": "2.7.5", 9 | "dependencies": { 10 | "eventsource": "^2.0.2", 11 | "mqtt": "^5.9.1", 12 | "request": "^2.88.2", 13 | "viz.js": "^2.1.2" 14 | }, 15 | "keywords": [ 16 | "zigbee2mqtt", 17 | "z2m", 18 | "mqtt", 19 | "node-red" 20 | ], 21 | "license": "GPL", 22 | "node-red": { 23 | "version": ">=1.3.0", 24 | "nodes": { 25 | "in": "nodes/in.js", 26 | "get": "nodes/get.js", 27 | "out": "nodes/out.js", 28 | "bridge": "nodes/bridge.js", 29 | "server": "nodes/server.js", 30 | "api": "api.js" 31 | } 32 | }, 33 | "engines": { 34 | "node": ">=12.0.0" 35 | }, 36 | "homepage": "https://github.com/andreypopov/node-red-contrib-zigbee2mqtt", 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/andreypopov/node-red-contrib-zigbee2mqtt.git" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/andreypopov/node-red-contrib-zigbee2mqtt/issues/" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /readme/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/readme/1.png -------------------------------------------------------------------------------- /readme/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/readme/2.png -------------------------------------------------------------------------------- /readme/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/readme/3.png -------------------------------------------------------------------------------- /readme/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/readme/4.png -------------------------------------------------------------------------------- /readme/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/readme/5.png -------------------------------------------------------------------------------- /readme/options.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreypopov/node-red-contrib-zigbee2mqtt/bbb0254b9f5bd7c9bc49ba6ed52f86bc79fe4f1c/readme/options.gif -------------------------------------------------------------------------------- /resources/Zigbee2mqttHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | class Zigbee2mqttHelper { 5 | static generateSelector(topic) { 6 | var transliterate = function(text) { 7 | text = text 8 | .replace(/\u0401/g, 'YO') 9 | .replace(/\u0419/g, 'I') 10 | .replace(/\u0426/g, 'TS') 11 | .replace(/\u0423/g, 'U') 12 | .replace(/\u041A/g, 'K') 13 | .replace(/\u0415/g, 'E') 14 | .replace(/\u041D/g, 'N') 15 | .replace(/\u0413/g, 'G') 16 | .replace(/\u0428/g, 'SH') 17 | .replace(/\u0429/g, 'SCH') 18 | .replace(/\u0417/g, 'Z') 19 | .replace(/\u0425/g, 'H') 20 | .replace(/\u042A/g, '') 21 | .replace(/\u0451/g, 'yo') 22 | .replace(/\u0439/g, 'i') 23 | .replace(/\u0446/g, 'ts') 24 | .replace(/\u0443/g, 'u') 25 | .replace(/\u043A/g, 'k') 26 | .replace(/\u0435/g, 'e') 27 | .replace(/\u043D/g, 'n') 28 | .replace(/\u0433/g, 'g') 29 | .replace(/\u0448/g, 'sh') 30 | .replace(/\u0449/g, 'sch') 31 | .replace(/\u0437/g, 'z') 32 | .replace(/\u0445/g, 'h') 33 | .replace(/\u044A/g, "'") 34 | .replace(/\u0424/g, 'F') 35 | .replace(/\u042B/g, 'I') 36 | .replace(/\u0412/g, 'V') 37 | .replace(/\u0410/g, 'a') 38 | .replace(/\u041F/g, 'P') 39 | .replace(/\u0420/g, 'R') 40 | .replace(/\u041E/g, 'O') 41 | .replace(/\u041B/g, 'L') 42 | .replace(/\u0414/g, 'D') 43 | .replace(/\u0416/g, 'ZH') 44 | .replace(/\u042D/g, 'E') 45 | .replace(/\u0444/g, 'f') 46 | .replace(/\u044B/g, 'i') 47 | .replace(/\u0432/g, 'v') 48 | .replace(/\u0430/g, 'a') 49 | .replace(/\u043F/g, 'p') 50 | .replace(/\u0440/g, 'r') 51 | .replace(/\u043E/g, 'o') 52 | .replace(/\u043B/g, 'l') 53 | .replace(/\u0434/g, 'd') 54 | .replace(/\u0436/g, 'zh') 55 | .replace(/\u044D/g, 'e') 56 | .replace(/\u042F/g, 'Ya') 57 | .replace(/\u0427/g, 'CH') 58 | .replace(/\u0421/g, 'S') 59 | .replace(/\u041C/g, 'M') 60 | .replace(/\u0418/g, 'I') 61 | .replace(/\u0422/g, 'T') 62 | .replace(/\u042C/g, "'") 63 | .replace(/\u0411/g, 'B') 64 | .replace(/\u042E/g, 'YU') 65 | .replace(/\u044F/g, 'ya') 66 | .replace(/\u0447/g, 'ch') 67 | .replace(/\u0441/g, 's') 68 | .replace(/\u043C/g, 'm') 69 | .replace(/\u0438/g, 'i') 70 | .replace(/\u0442/g, 't') 71 | .replace(/\u044C/g, "'") 72 | .replace(/\u0431/g, 'b') 73 | .replace(/\u044E/g, 'yu'); 74 | 75 | return text; 76 | }; 77 | 78 | topic = transliterate(topic); 79 | return topic.split('/').join('_').replace(/[^a-zA-Z0-9_-]/g, ''); 80 | } 81 | 82 | static convertRange(value, r1, r2) { 83 | var val = Math.ceil((value - r1[0]) * (r2[1] - r2[0]) / (r1[1] - r1[0]) + r2[0]); 84 | if (val < r2[0]) val = r2[0]; 85 | if (val > r2[1]) val = r2[1]; 86 | return val; 87 | } 88 | 89 | static isJson(str) { 90 | try { 91 | JSON.parse(str); 92 | } catch (e) { 93 | return false; 94 | } 95 | return true; 96 | } 97 | 98 | static cie2rgb(x, y, brightness) { 99 | //Set to maximum brightness if no custom value was given (Not the slick ECMAScript 6 way for compatibility reasons) 100 | if (brightness === undefined) { 101 | brightness = 254; 102 | } 103 | 104 | var z = 1.0 - x - y; 105 | var Y = (brightness / 254).toFixed(2); 106 | var X = (Y / y) * x; 107 | var Z = (Y / y) * z; 108 | 109 | //Convert to RGB using Wide RGB D65 conversion 110 | var red = X * 1.656492 - Y * 0.354851 - Z * 0.255038; 111 | var green = -X * 0.707196 + Y * 1.655397 + Z * 0.036152; 112 | var blue = X * 0.051713 - Y * 0.121364 + Z * 1.011530; 113 | 114 | //If red, green or blue is larger than 1.0 set it back to the maximum of 1.0 115 | if (red > blue && red > green && red > 1.0) { 116 | 117 | green = green / red; 118 | blue = blue / red; 119 | red = 1.0; 120 | } else if (green > blue && green > red && green > 1.0) { 121 | 122 | red = red / green; 123 | blue = blue / green; 124 | green = 1.0; 125 | } else if (blue > red && blue > green && blue > 1.0) { 126 | 127 | red = red / blue; 128 | green = green / blue; 129 | blue = 1.0; 130 | } 131 | 132 | //Reverse gamma correction 133 | red = red <= 0.0031308 ? 12.92 * red : (1.0 + 0.055) * Math.pow(red, (1.0 / 2.4)) - 0.055; 134 | green = green <= 0.0031308 ? 12.92 * green : (1.0 + 0.055) * Math.pow(green, (1.0 / 2.4)) - 0.055; 135 | blue = blue <= 0.0031308 ? 12.92 * blue : (1.0 + 0.055) * Math.pow(blue, (1.0 / 2.4)) - 0.055; 136 | 137 | //Convert normalized decimal to decimal 138 | red = Math.round(red * 255); 139 | green = Math.round(green * 255); 140 | blue = Math.round(blue * 255); 141 | 142 | if (isNaN(red)) 143 | red = 0; 144 | 145 | if (isNaN(green)) 146 | green = 0; 147 | 148 | if (isNaN(blue)) 149 | blue = 0; 150 | 151 | return { 152 | r: red, 153 | g: green, 154 | b: blue 155 | }; 156 | } 157 | 158 | static rgb2hsv(r, g, b) { 159 | let rabs, gabs, babs, rr, gg, bb, h, s, v, diff, diffc, percentRoundFn; 160 | rabs = r / 255; 161 | gabs = g / 255; 162 | babs = b / 255; 163 | v = Math.max(rabs, gabs, babs), 164 | diff = v - Math.min(rabs, gabs, babs); 165 | diffc = c => (v - c) / 6 / diff + 1 / 2; 166 | percentRoundFn = num => Math.round(num * 100) / 100; 167 | if (diff == 0) { 168 | h = s = 0; 169 | } else { 170 | s = diff / v; 171 | rr = diffc(rabs); 172 | gg = diffc(gabs); 173 | bb = diffc(babs); 174 | 175 | if (rabs === v) { 176 | h = bb - gg; 177 | } else if (gabs === v) { 178 | h = (1 / 3) + rr - bb; 179 | } else if (babs === v) { 180 | h = (2 / 3) + gg - rr; 181 | } 182 | if (h < 0) { 183 | h += 1; 184 | } else if (h > 1) { 185 | h -= 1; 186 | } 187 | } 188 | return { 189 | h: Math.round(h * 360), 190 | s: percentRoundFn(s * 100), 191 | v: percentRoundFn(v * 100) 192 | }; 193 | } 194 | 195 | static payload2homekit(payload) { 196 | var msg = {}; 197 | 198 | if (!payload) return msg; 199 | 200 | //Lightbulb 201 | if ("brightness" in payload) { 202 | if ("state" in payload && payload.state === "OFF") { 203 | msg["Lightbulb"] = {"On": false}; 204 | if ("color_temp" in payload) { 205 | msg["Lightbulb_CT"] = {"On": false}; 206 | } 207 | if ("color" in payload) { 208 | msg["Lightbulb_RGB"] = {"On": false}; 209 | } 210 | if ("color" in payload && "color_temp" in payload) { 211 | msg["Lightbulb_RGB_CT"] = {"On": false}; 212 | } 213 | } else { 214 | 215 | var hue = null; 216 | var sat = null; 217 | if ("color" in payload && "hue" in payload.color && "saturation" in payload.color) { 218 | hue = payload.color.hue; 219 | sat = payload.color.saturation; 220 | } else if ("color" in payload && "x" in payload.color) { 221 | var rgb = Zigbee2mqttHelper.cie2rgb(payload.color.x, payload.color.y, payload.brightness); 222 | var hsv = Zigbee2mqttHelper.rgb2hsv(rgb.r, rgb.g, rgb.b); 223 | hue = hsv.h; 224 | sat = hsv.s; 225 | } 226 | var bri = Zigbee2mqttHelper.convertRange(parseInt(payload.brightness), [0, 255], [0, 100]); 227 | var ct = "color_temp" in payload ? Zigbee2mqttHelper.convertRange(parseInt(payload.color_temp), [150, 500], [150, 500]) : null; 228 | 229 | msg["Lightbulb"] = { 230 | "On": true, 231 | "Brightness": bri 232 | } 233 | if ("color_temp" in payload) { 234 | msg["Lightbulb_CT"] = { 235 | "On": true, 236 | "Brightness": bri, 237 | "ColorTemperature": ct 238 | } 239 | } 240 | if ("color" in payload) { 241 | msg["Lightbulb_RGB"] = { 242 | "On": true, 243 | "Brightness": bri, 244 | "Hue": hue, 245 | "Saturation": sat 246 | } 247 | } 248 | if ("color" in payload && "color_temp" in payload) { 249 | msg["Lightbulb_RGB_CT"] = { 250 | "On": true, 251 | "Brightness": bri, 252 | "Hue": hue, 253 | "Saturation": sat, 254 | "ColorTemperature": ct 255 | } 256 | } 257 | } 258 | } 259 | 260 | //LockMechanism 261 | if ("state" in payload && (payload.state === "LOCK" || payload.state === "UNLOCK")) { 262 | msg["LockMechanism"] = { 263 | "LockCurrentState": payload.state === "LOCK" ? 1 : 0, 264 | "LockTargetState": payload.state === "LOCK" ? 1 : 0 265 | }; 266 | } 267 | 268 | 269 | 270 | // 0: "stopped" 271 | // 1: "opening" 272 | // 2: "closing" 273 | // public static readonly DECREASING = 0; 274 | // public static readonly INCREASING = 1; 275 | // public static readonly STOPPED = 2; 276 | if ('position' in payload && 'motor_state' in payload) { 277 | let motor_state = null; 278 | let position = 0; //closed 279 | switch (payload.motor_state) { 280 | case 'closing': 281 | motor_state = 0; 282 | position = 0; 283 | break; 284 | case 'opening': 285 | motor_state = 1; 286 | position = 100; 287 | break; 288 | case 'stopped': 289 | default: 290 | motor_state = 2; 291 | position = parseInt(payload.position) || 0; 292 | break; 293 | } 294 | 295 | msg["Window"] = msg["WindowCovering"] = msg["Door"] = { 296 | "CurrentPosition": parseInt(payload.position), 297 | "TargetPosition": position, 298 | "PositionState": motor_state 299 | }; 300 | 301 | } else if ('position' in payload && 'running' in payload) { //old?? 302 | msg["Window"] = msg["WindowCovering"] = msg["Door"] = { 303 | "CurrentPosition": parseInt(payload.position), 304 | "TargetPosition": parseInt(payload.position), 305 | "PositionState": payload.running ? 1 : 2 //increasing=1, stopped=2 306 | }; 307 | } else if ('position' in payload) { //no position in payload (eg: ikea) 308 | msg["Window"] = msg["WindowCovering"] = msg["Door"] = { 309 | "CurrentPosition": parseInt(payload.position), 310 | "TargetPosition": parseInt(payload.position), 311 | "PositionState": 2 //stopped=2, there is no way to get current motor status 312 | }; 313 | } 314 | 315 | //TemperatureSensor 316 | if ('temperature' in payload) { 317 | msg["TemperatureSensor"] = { 318 | "CurrentTemperature": parseFloat(payload.temperature) 319 | }; 320 | } 321 | 322 | //HumiditySensor 323 | if ('humidity' in payload) { 324 | msg["HumiditySensor"] = { 325 | "CurrentRelativeHumidity": parseFloat(payload.humidity) 326 | }; 327 | } 328 | 329 | //LightSensor 330 | if ('illuminance_lux' in payload) { 331 | msg["LightSensor"] = { 332 | "CurrentAmbientLightLevel": parseInt(payload.illuminance_lux) 333 | }; 334 | } 335 | 336 | //ContactSensor 337 | if ('contact' in payload) { 338 | msg["ContactSensor"] = { 339 | "ContactSensorState": payload.contact ? 0 : 1 340 | }; 341 | msg["ContactSensor_Inverse"] = { 342 | "ContactSensorState": payload.contact ? 1 : 0 343 | }; 344 | } 345 | 346 | //MotionSensor, OccupancySensor 347 | if ('occupancy' in payload) { 348 | msg["MotionSensor"] = { 349 | "MotionDetected": payload.occupancy 350 | }; 351 | msg["OccupancySensor"] = { 352 | "OccupancyDetected":payload.occupancy?1:0 353 | }; 354 | } 355 | 356 | //WaterLeak 357 | if ('water_leak' in payload) { 358 | msg["LeakSensor"] = { 359 | "LeakDetected": payload.water_leak ? 1 : 0 360 | }; 361 | } 362 | 363 | //Smoke 364 | if ('smoke' in payload) { 365 | msg["SmokeSensor"] = { 366 | "SmokeDetected": payload.smoke ? 1 : 0 367 | }; 368 | } 369 | 370 | //Battery 371 | // if ("powerSource" in device && "Battery" == device.powerSource && "battery" in payload && parseInt(payload.battery)>0) { 372 | if ('battery' in payload) { 373 | msg["Battery"] = { 374 | "BatteryLevel": parseInt(payload.battery), 375 | "StatusLowBattery": parseInt(payload.battery) <= 15 ? 1 : 0 376 | }; 377 | } 378 | 379 | //Switch 380 | if ("state" in payload && (payload.state === "ON" || payload.state === "OFF")) { 381 | msg["Switch"] = { 382 | "On": payload.state === "ON" 383 | }; 384 | } 385 | 386 | return msg; 387 | } 388 | 389 | static formatPayload(payload, device) { 390 | var node = this; 391 | var result = {}; 392 | 393 | //convert XY to RGB, HSV 394 | if (payload && "color" in payload && "x" in payload.color) { 395 | var bri = "brightness" in payload ? payload.brightness : 255; 396 | var rgb = Zigbee2mqttHelper.cie2rgb(payload.color.x, payload.color.y, bri); 397 | var hsv = Zigbee2mqttHelper.rgb2hsv(rgb.r, rgb.g, rgb.b); 398 | result['color'] = { 399 | "rgb": rgb, 400 | "hsv": hsv 401 | }; 402 | } 403 | return result; 404 | } 405 | 406 | static formatMath(data) { 407 | var result = {}; 408 | 409 | for (var i in data) { 410 | for (var key in data[i]) { 411 | var val = data[i][key]; 412 | if (Zigbee2mqttHelper.isNumber(val)) { 413 | if (!(key in result)) result[key] = {"count": 0, "avg": 0, "min": null, "max": null, "sum": 0}; 414 | 415 | result[key]["count"] += 1; 416 | result[key]["sum"] = Math.round((result[key]["sum"] + val) * 100) / 100; 417 | result[key]["min"] = result[key]["min"] == null || val < result[key]["min"] ? val : result[key]["min"]; 418 | result[key]["max"] = result[key]["max"] == null || val > result[key]["max"] ? val : result[key]["max"]; 419 | result[key]["avg"] = Math.round((result[key]["sum"] / result[key]["count"]) * 100) / 100; 420 | } 421 | } 422 | } 423 | 424 | return result; 425 | } 426 | 427 | static statusUpdatedAt() { 428 | return ' [' + new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString() + ']' 429 | } 430 | 431 | static isNumber(n) 432 | { 433 | return Zigbee2mqttHelper.isInt(n) || Zigbee2mqttHelper.isFloat(n); 434 | } 435 | 436 | static isInt(n) 437 | { 438 | if (n === 'true' || n === true || n === 'false' || n === false) return false; 439 | return n !== "" && !isNaN(n) && Math.round(n) === n; 440 | } 441 | 442 | static isFloat(n){ 443 | if (n === 'true' || n === true || n === 'false' || n === false) return false; 444 | return n !== "" && !isNaN(n) && Math.round(n) !== n; 445 | } 446 | 447 | // static objectsDiff(obj1, obj2) { 448 | // if (obj1 && typeof(obj1) === 'object' && obj2 && typeof(obj2) === 'object') { 449 | // var diffObj = Array.isArray(obj2) ? [] : {} 450 | // Object.getOwnPropertyNames(obj2).forEach(function(prop) { 451 | // if (typeof obj2[prop] === 'object') { 452 | // diffObj[prop] = deepCompare(obj1[prop], obj2[prop]) 453 | // // if it's an array with only length property => empty array => delete 454 | // // or if it's an object with no own properties => delete 455 | // if (Array.isArray(diffObj[prop]) && Object.getOwnPropertyNames(diffObj[prop]).length === 1 || 456 | // Object.getOwnPropertyNames(diffObj[prop]).length === 0) { 457 | // delete diffObj[prop] 458 | // } 459 | // } else if (obj1[prop] !== obj2[prop]) { 460 | // diffObj[prop] = obj2[prop] 461 | // } 462 | // }); 463 | // return diffObj 464 | // } else { 465 | // return null; 466 | // } 467 | // } 468 | } 469 | 470 | module.exports = Zigbee2mqttHelper; 471 | -------------------------------------------------------------------------------- /resources/css/common.css: -------------------------------------------------------------------------------- 1 | .form-row label.l-width { 2 | width: 25%; 3 | min-width: 120px; 4 | } 5 | .form-row label.a-width { 6 | width: auto; 7 | } 8 | .s-width {width:70% !important;} 9 | 10 | .ms-choice { 11 | height:34px; 12 | border: 1px solid rgb(204, 204, 204); 13 | line-height: 34px !important; 14 | } 15 | .ms-search input { 16 | width:100% !important; 17 | } 18 | .ms-drop li label { 19 | width:20px !important; 20 | } 21 | .ms-drop li span { 22 | padding-left:5px !important; 23 | } 24 | button.active { 25 | background-color: #ddd; 26 | } 27 | .help_block { 28 | display: none; 29 | } 30 | .help-tips { 31 | font-style: italic; 32 | font-size: 12px; 33 | } 34 | 35 | .red-ui-editor .form-horizontal li.token-input-input-token-facebook input { 36 | height:23px !important; 37 | width: 50px; 38 | padding: 1px 4px; 39 | background-color: #fafafa; 40 | border: 1px dashed #91bacf; 41 | margin: 3px; 42 | border-radius: 3px; 43 | -moz-border-radius: 3px; 44 | -webkit-border-radius: 3px; 45 | font-size: 12px; 46 | } 47 | li.token-input-token-facebook { 48 | height:19px !important; 49 | } 50 | .tokeninput-label { 51 | float:left; 52 | line-height: 23px; 53 | padding: 3px 5px 0 0; 54 | } 55 | 56 | .ms-drop ul > li .disabled {color: #2a2a2a !important;font-weight: bold!important;opacity: 1!important;} 57 | -------------------------------------------------------------------------------- /resources/css/multiple-select.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | /** 3 | * @author zhixin wen 4 | */ 5 | .ms-offscreen { 6 | clip: rect(0 0 0 0) !important; 7 | width: 1px !important; 8 | height: 1px !important; 9 | border: 0 !important; 10 | margin: 0 !important; 11 | padding: 0 !important; 12 | overflow: hidden !important; 13 | position: absolute !important; 14 | outline: 0 !important; 15 | left: auto !important; 16 | top: auto !important; } 17 | 18 | .ms-parent { 19 | display: inline-block; 20 | position: relative; 21 | vertical-align: middle; } 22 | 23 | .ms-choice { 24 | display: block; 25 | width: 100%; 26 | height: 26px; 27 | padding: 0; 28 | overflow: hidden; 29 | cursor: pointer; 30 | border: 1px solid #aaa; 31 | text-align: left; 32 | white-space: nowrap; 33 | line-height: 26px; 34 | color: #444; 35 | text-decoration: none; 36 | border-radius: 4px; 37 | background-color: #fff; } 38 | .ms-choice.disabled { 39 | background-color: #f4f4f4; 40 | background-image: none; 41 | border: 1px solid #ddd; 42 | cursor: default; } 43 | .ms-choice > span { 44 | position: absolute; 45 | top: 0; 46 | left: 0; 47 | right: 20px; 48 | white-space: nowrap; 49 | overflow: hidden; 50 | text-overflow: ellipsis; 51 | display: block; 52 | padding-left: 8px; } 53 | .ms-choice > span.placeholder { 54 | color: #999; } 55 | .ms-choice > div.icon-close { 56 | position: absolute; 57 | top: 0px; 58 | right: 16px; 59 | height: 100%; 60 | width: 16px; } 61 | .ms-choice > div.icon-close:before { 62 | content: '×'; 63 | color: #888; 64 | font-weight: bold; 65 | position: absolute; 66 | top: 50%; 67 | margin-top: -14px; } 68 | .ms-choice > div.icon-close:hover:before { 69 | color: #333; } 70 | .ms-choice > div.icon-caret { 71 | position: absolute; 72 | width: 0; 73 | height: 0; 74 | top: 50%; 75 | right: 8px; 76 | margin-top: -2px; 77 | border-color: #888 transparent transparent transparent; 78 | border-style: solid; 79 | border-width: 5px 4px 0 4px; } 80 | .ms-choice > div.icon-caret.open { 81 | border-color: transparent transparent #888 transparent; 82 | border-width: 0 4px 5px 4px; } 83 | 84 | .ms-drop { 85 | width: auto; 86 | min-width: 100%; 87 | overflow: hidden; 88 | display: none; 89 | margin-top: -1px; 90 | padding: 0; 91 | position: absolute; 92 | z-index: 1000; 93 | background: #fff; 94 | color: #000; 95 | border: 1px solid #aaa; 96 | border-radius: 4px; } 97 | .ms-drop.bottom { 98 | top: 100%; 99 | box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15); } 100 | .ms-drop.top { 101 | bottom: 100%; 102 | box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15); } 103 | 104 | .ms-search { 105 | display: inline-block; 106 | margin: 0; 107 | min-height: 26px; 108 | padding: 2px; 109 | position: relative; 110 | white-space: nowrap; 111 | width: 100%; 112 | z-index: 10000; 113 | box-sizing: border-box; } 114 | .ms-search input { 115 | width: 100%; 116 | height: auto !important; 117 | min-height: 24px; 118 | padding: 0 5px; 119 | margin: 0; 120 | outline: 0; 121 | font-family: sans-serif; 122 | border: 1px solid #aaa; 123 | border-radius: 5px; 124 | box-shadow: none; } 125 | 126 | .ms-drop ul { 127 | overflow: auto; 128 | margin: 0; 129 | padding: 0; } 130 | .ms-drop ul > li { 131 | list-style: none; 132 | display: list-item; 133 | background-image: none; 134 | position: static; 135 | padding: .25rem 8px; } 136 | .ms-drop ul > li .disabled { 137 | font-weight: normal !important; 138 | opacity: .35; 139 | filter: Alpha(Opacity=35); 140 | cursor: default; } 141 | .ms-drop ul > li.multiple { 142 | display: block; 143 | float: left; } 144 | .ms-drop ul > li.group { 145 | clear: both; } 146 | .ms-drop ul > li.multiple label { 147 | width: 100%; 148 | display: block; 149 | white-space: nowrap; 150 | overflow: hidden; 151 | text-overflow: ellipsis; } 152 | .ms-drop ul > li label { 153 | position: relative; 154 | padding-left: 1.25rem; 155 | margin-bottom: 0; 156 | font-weight: normal; 157 | display: block; 158 | white-space: nowrap; 159 | cursor: pointer; } 160 | .ms-drop ul > li label.optgroup { 161 | font-weight: bold; } 162 | .ms-drop ul > li.hide-radio { 163 | padding: 0; } 164 | .ms-drop ul > li.hide-radio:focus, .ms-drop ul > li.hide-radio:hover { 165 | background-color: #f8f9fa; } 166 | .ms-drop ul > li.hide-radio.selected { 167 | color: #fff; 168 | background-color: #007bff; } 169 | .ms-drop ul > li.hide-radio label { 170 | margin-bottom: 0; 171 | padding: 5px 8px; } 172 | .ms-drop ul > li.hide-radio input { 173 | display: none; } 174 | .ms-drop ul > li.option-level-1 label { 175 | padding-left: 28px; } 176 | 177 | .ms-drop input[type="radio"], .ms-drop input[type="checkbox"] { 178 | position: absolute; 179 | margin-top: .3rem; 180 | margin-left: -1.25rem; } 181 | 182 | .ms-drop .ms-no-results { 183 | display: none; } 184 | -------------------------------------------------------------------------------- /resources/js/node-red-contrib-zigbee2mqtt-helpers.js: -------------------------------------------------------------------------------- 1 | class Zigbee2MqttEditor { 2 | constructor(node, config = {}) { 3 | this.node = node; 4 | this.devices = null; 5 | this.config = Object.assign( { 6 | allow_empty:false 7 | }, config); 8 | this.device_id = node.device_id||null; 9 | this.property = node.state||null; 10 | this.optionsValue = node.optionsValue||null; 11 | this.optionsType = node.optionsType||null; 12 | this.refresh = false; 13 | 14 | return this; 15 | } 16 | 17 | bind() { 18 | let that = this; 19 | that.getRefreshBtn().off('click').on('click', () => { 20 | that.refresh = true; 21 | that.build(); 22 | }); 23 | that.getServerInput().off('change').on('change', (e) => { 24 | // console.log('bind: getServerInput'); 25 | that.property = null; 26 | that.refresh = true; 27 | that.build(); 28 | }); 29 | that.getDeviceIdInput().off('change').on('change', () => { 30 | that.device_id = that.getDeviceIdInput().multipleSelect('getSelects', 'value'); 31 | if (!that.isMultiple()) { //no need to build for multiple 32 | // console.log('bind: getDeviceIdInput'); 33 | that.build(); 34 | } else { 35 | that.setFriendlyName(); 36 | } 37 | }); 38 | if (that.getDeviceOptionsInput()) { 39 | that.getDeviceOptionsInput().off('change').on('change', (event, type, value) => { 40 | // console.log('bind: getDeviceOptionsInput'); 41 | that.optionsValue = value; 42 | that.optionsType = type; 43 | that.buildDeviceOptionsHelpBlock(); 44 | }); 45 | } 46 | that.getEnableMultipleCheckbox().off('change').on('change', (e) => { 47 | // that.property = null; 48 | // that.refresh = true; 49 | // console.log('bind: getEnableMultipleCheckbox'); 50 | that.build(); 51 | }); 52 | } 53 | 54 | async build() { 55 | let that = this; 56 | // console.log('build : '+(this.refresh?'true':false)); 57 | await that.buildDeviceIdInput().then(()=>{ 58 | that.buildDevicePropertyInput(); 59 | that.buildDeviceOptionsInput(); 60 | }); 61 | that.bind(); 62 | } 63 | 64 | async buildDeviceIdInput() { 65 | let that = this; 66 | that.getFilterChanges().closest('.form-row').toggle(!that.isMultiple()); 67 | // console.log('BUILD buildDeviceIdInput'); 68 | 69 | let params = { 70 | single: !that.isMultiple(), 71 | minimumCountSelected: !that.isMultiple()?1:0, 72 | numberDisplayed: 1, 73 | maxHeight: 300, 74 | dropWidth: 320, 75 | width: 320, 76 | filter: true, 77 | formatAllSelected:function(){return RED._("node-red-contrib-zigbee2mqtt/server:editor.select_device")} 78 | }; 79 | if (that.config.allow_empty && !that.isMultiple()) { 80 | params.formatAllSelected = function(){return RED._("node-red-contrib-zigbee2mqtt/server:editor.msg_topic")}; 81 | } 82 | 83 | that.getDeviceIdInput().children().remove(); 84 | that.getDeviceIdInput().multipleSelect('destroy').multipleSelect(params).multipleSelect('disable'); 85 | 86 | let data = await that.getDevices(); 87 | 88 | if (that.config.allow_empty && !that.isMultiple()) { 89 | that.getDeviceIdInput().html(''); 90 | } 91 | 92 | let html = ''; 93 | 94 | //groups 95 | let groups = data[1]; 96 | if (groups.length) { 97 | html = $('', {label: RED._("node-red-contrib-zigbee2mqtt/server:editor.groups")}); 98 | html.appendTo(that.getDeviceIdInput()); 99 | $.each(groups, function(index, value) { 100 | let text = ''; 101 | if ("devices" in value && typeof (value.devices) != 'undefined' && value.devices.length > 0) { 102 | text = ' (' + value.devices.length + ')'; 103 | } 104 | $('') 105 | .appendTo(html); 106 | }); 107 | } 108 | 109 | //devices 110 | let devices = data[0]; 111 | if (devices.length) { 112 | html = $('', {label: RED._("node-red-contrib-zigbee2mqtt/server:editor.devices")}); 113 | html.appendTo(that.getDeviceIdInput()); 114 | $.each(devices, function(index, value) { 115 | var model = ''; 116 | if ("definition" in value && value.definition && "model" in value.definition && typeof (value.definition.model) !== undefined) { 117 | model = ' (' + value.definition.model + ')'; 118 | } 119 | $('') 120 | .appendTo(html); 121 | }); 122 | } 123 | 124 | that.getDeviceIdInput().multipleSelect('enable'); 125 | that.getDeviceIdInput().multipleSelect('refresh'); 126 | 127 | that.setDeviceValue(); 128 | that.setFriendlyName(); 129 | return this; 130 | } 131 | 132 | async buildDevicePropertyInput() { 133 | let that = this; 134 | if (!that.getDevicePropertyInput()) return; 135 | that.getDevicePropertyInput().closest('.form-row').toggle(!that.isMultiple()); 136 | if (that.isMultiple()) return; 137 | 138 | // console.log('BUILD buildDevicePropertyInput'); 139 | 140 | that.getDevicePropertyInput().children().remove(); 141 | that.getDevicePropertyInput().multipleSelect('destroy').multipleSelect({ 142 | numberDisplayed: 1, 143 | dropWidth: 320, 144 | width: 320, 145 | single: !(typeof $(this).attr('multiple') !== typeof undefined && $(this).attr('multiple') !== false) 146 | }).multipleSelect('disable'); 147 | 148 | that.getDevicePropertyInput().html(''); 149 | 150 | let html = ''; 151 | let device = that.getDevice(); 152 | 153 | if (device && 'definition' in device && device.definition && 'exposes' in device.definition) { 154 | html = $('', {label: RED._("node-red-contrib-zigbee2mqtt/server:editor.zigbee2mqtt")}); 155 | html.appendTo(that.getDevicePropertyInput()); 156 | 157 | $.each(device.definition.exposes, function(index, value) { 158 | if ('features' in value) { 159 | $.each(value.features, function(index2, value2) { 160 | if ('property' in value2) { 161 | $('') 162 | .appendTo(html); 163 | } 164 | }); 165 | } else if ('property' in value) { 166 | $('') 167 | .appendTo(html); 168 | } 169 | }); 170 | } 171 | 172 | if (device && 'homekit' in device && device.homekit && Object.keys(device.homekit).length) { 173 | html = $('', {label: RED._("node-red-contrib-zigbee2mqtt/server:editor.homekit")}); 174 | html.appendTo(that.getDevicePropertyInput()); 175 | 176 | $.each(device.homekit, function (index, value) { 177 | $('').appendTo(html); 178 | }); 179 | } 180 | that.getDevicePropertyInput().multipleSelect('enable'); 181 | if (that.getDevicePropertyInput().find('option[value='+that.property+']').length) { 182 | that.getDevicePropertyInput().val(that.property); 183 | } else { 184 | that.getDevicePropertyInput().val(that.getDevicePropertyInput().find('option').eq(0).attr('value')); 185 | } 186 | that.getDevicePropertyInput().multipleSelect('refresh'); 187 | } 188 | 189 | buildDeviceOptionsInput() { 190 | let that = this; 191 | if (!that.getDeviceOptionsInput()) return; 192 | that.getDeviceOptionsTypeHelpBlock().hide().find('div').text('').closest('.form-tips').find('span').text(''); 193 | that.getDeviceOptionsInput().closest('.form-row').toggle(!that.isMultiple()); 194 | if (that.isMultiple()) return; 195 | 196 | // console.log('BUILD buildDeviceOptionsInput'); 197 | let device = that.getDevice(); 198 | let options = []; 199 | options.push({'value': 'nothing', 'label': RED._("node-red-contrib-zigbee2mqtt/server:editor.nothing"), options:['']}); 200 | options.push('msg'); 201 | options.push('json'); 202 | if (device && 'definition' in device && device.definition && 'options' in device.definition) { 203 | $.each(device.definition.options, function(k, v) { 204 | options.push({'value': v.property, 'label': v.name}); 205 | }); 206 | } 207 | that.getDeviceOptionsInput().typedInput({ 208 | default: 'nothing', 209 | value: that.optionsType, 210 | typeField: that.getDeviceOptionsTypeInput(), 211 | }); 212 | that.getDeviceOptionsInput().typedInput('types', options); 213 | that.getDeviceOptionsInput().typedInput('type', that.optionsType || 'nothing'); 214 | that.getDeviceOptionsInput().typedInput('value', that.optionsValue || ''); 215 | that.buildDeviceOptionsHelpBlock(); 216 | } 217 | 218 | buildDeviceOptionsHelpBlock() { 219 | let that = this; 220 | if (!that.getDeviceOptionsTypeHelpBlock()) return; 221 | 222 | that.getDeviceOptionsTypeHelpBlock().hide().find('div').text('').closest('.form-tips').find('span').text(''); 223 | if (that.isMultiple()) return; 224 | 225 | // console.log('BUILD buildDeviceOptionsHelpBlock'); 226 | 227 | let device = that.getDevice(); 228 | let selectedOption = null; 229 | if (device && 'definition' in device && device.definition && 'options' in device.definition) { 230 | $.each(device.definition.options, function(k, v) { 231 | if ('json' === that.optionsType) { 232 | let json = {}; 233 | $.each(device.definition.options, function(k, v2) { 234 | if ('property' in v2) { 235 | let defaultVal = ''; 236 | if ('type' in v2) { 237 | if (v2.type==='numeric') { 238 | defaultVal = 0; 239 | if ('value_min' in v2) { 240 | defaultVal = v2.value_min; 241 | } 242 | } else if (v2.type==='binary') { 243 | defaultVal = false; 244 | } 245 | } 246 | json[v2.property] = defaultVal; 247 | } 248 | }); 249 | selectedOption = {'name':'JSON', 'description':JSON.stringify(json, null, 4)}; 250 | return false; 251 | } 252 | if (v.property === that.optionsType) { 253 | selectedOption = v; 254 | return false; 255 | } 256 | }); 257 | } 258 | 259 | if (selectedOption && 'description' in selectedOption && selectedOption.description) { 260 | that.getDeviceOptionsTypeHelpBlock().show().find('div').text(selectedOption.name).closest('.form-tips').find('span').text(selectedOption.description); 261 | } 262 | } 263 | 264 | async getDevices() { 265 | let that = this; 266 | if (that.devices === null || that.refresh) { 267 | const response = await fetch('zigbee2mqtt/getDevices?' + new URLSearchParams({ 268 | controllerID: that.getServerInput().val() 269 | }).toString(), { 270 | method: 'GET', 271 | cache: 'no-cache', 272 | headers: { 273 | 'Content-Type': 'application/json' 274 | } 275 | }); 276 | that.refresh = false; 277 | that.devices = await response.json(); 278 | return that.devices; 279 | } else { 280 | return await new Promise(function(resolve, reject) { 281 | resolve(that.devices); 282 | }); 283 | } 284 | } 285 | 286 | getDevice() { 287 | let that = this; 288 | let devices = that.devices[0]; 289 | let device = null; 290 | 291 | if (devices.length && that.device_id) { 292 | let selectedDevice = typeof(that.device_id) === 'object' ? that.device_id[0] : that.device_id; 293 | $.each(devices, function (index, item) { 294 | if (item.ieee_address === selectedDevice) { 295 | device = item; 296 | return false; 297 | } 298 | }); 299 | } 300 | return device; 301 | } 302 | 303 | getDeviceIdInput() { 304 | return $('#node-input-device_id'); 305 | } 306 | 307 | getDevicePropertyInput() { 308 | let $elem = $('#node-input-state'); 309 | return $elem.length?$elem:null; 310 | } 311 | 312 | getDeviceOptionsInput() { 313 | let $elem = $('#node-input-optionsValue'); 314 | return $elem.length?$elem:null; 315 | } 316 | 317 | getDeviceOptionsTypeInput() { 318 | let $elem = $('#node-input-optionsType'); 319 | return $elem.length?$elem:null; 320 | } 321 | 322 | getDeviceOptionsTypeHelpBlock() { 323 | return $('.optionsType_description'); 324 | } 325 | 326 | getDeviceFriendlyNameInput() { 327 | return $('#node-input-friendly_name'); 328 | } 329 | 330 | getServerInput() { 331 | return $('#node-input-server'); 332 | } 333 | 334 | getRefreshBtn() { 335 | return $('#force-refresh'); 336 | } 337 | 338 | getFilterChanges() { 339 | return $('#node-input-filterChanges'); 340 | } 341 | 342 | getEnableMultipleCheckbox() { 343 | return $('#node-input-enableMultiple'); 344 | } 345 | 346 | isMultiple() { 347 | return this.getEnableMultipleCheckbox().is(':checked'); 348 | } 349 | 350 | setDeviceValue() { 351 | let that = this; 352 | if (that.isMultiple()) { 353 | if (typeof(that.device_id) == 'string') { 354 | that.device_id = [that.device_id]; 355 | } 356 | if (that.device_id) { 357 | that.getDeviceIdInput().multipleSelect('setSelects', that.device_id); 358 | } 359 | } else if (that.device_id && that.device_id.length) { 360 | if (typeof(that.device_id) == 'object') { 361 | that.device_id = that.device_id[0]; //get the first device 362 | } 363 | if (that.getDeviceIdInput().find('option[value="'+that.device_id+'"]').length) { 364 | that.getDeviceIdInput().val(that.device_id); 365 | } 366 | // that.getDeviceIdInput().multipleSelect('check', that.device_id); //does not work 367 | that.getDeviceIdInput().multipleSelect('refresh'); 368 | } else { 369 | that.device_id = null; 370 | } 371 | } 372 | 373 | setFriendlyName() { 374 | let that = this; 375 | if (that.isMultiple()) { 376 | if (typeof(that.device_id) == 'string') { 377 | that.device_id = [that.device_id]; 378 | } 379 | if (!that.device_id) { 380 | that.device_id = []; 381 | } 382 | that.getDeviceFriendlyNameInput().val(that.device_id.length + ' ' + RED._("node-red-contrib-zigbee2mqtt/server:editor.selected")); 383 | } else if (that.device_id && that.device_id.length) { 384 | if (typeof(that.device_id) == 'object') { 385 | that.device_id = that.device_id[0]; //get the first device 386 | } 387 | if (that.getDeviceIdInput().find('option[value="'+that.device_id+'"]').length) { 388 | that.getDeviceFriendlyNameInput().val(that.getDeviceIdInput().multipleSelect('getSelects', 'text')); 389 | } 390 | } else { 391 | that.getDeviceFriendlyNameInput().val(''); 392 | } 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /resources/tokeninput/jquery.tokeninput.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Plugin: Tokenizing Autocomplete Text Entry 3 | * Version 1.6.0 4 | * 5 | * Copyright (c) 2009 James Smith (http://loopj.com) 6 | * Licensed jointly under the GPL and MIT licenses, 7 | * choose which one suits your project best! 8 | * 9 | */ 10 | 11 | (function ($) { 12 | // Default settings 13 | var DEFAULT_SETTINGS = { 14 | // Search settings 15 | method: "GET", 16 | contentType: "json", 17 | queryParam: "q", 18 | searchDelay: 300, 19 | minChars: 1, 20 | propertyToSearch: "name", 21 | jsonContainer: null, 22 | 23 | // Display settings 24 | hintText: "Type in a search term", 25 | noResultsText: "No results", 26 | searchingText: "Searching...", 27 | deleteText: "×", 28 | animateDropdown: true, 29 | 30 | // Tokenization settings 31 | tokenLimit: null, 32 | tokenDelimiter: ",", 33 | preventDuplicates: false, 34 | 35 | // Output settings 36 | tokenValue: "id", 37 | 38 | // Prepopulation settings 39 | prePopulate: null, 40 | processPrePopulate: false, 41 | 42 | // Manipulation settings 43 | idPrefix: "token-input-", 44 | 45 | // Formatters 46 | resultsFormatter: function(item){ return "
  • " + item[this.propertyToSearch]+ "
  • " }, 47 | tokenFormatter: function(item) { return "
  • " + item[this.propertyToSearch] + "

  • " }, 48 | 49 | // Callbacks 50 | onResult: null, 51 | onAdd: null, 52 | onDelete: null, 53 | onReady: null 54 | }; 55 | 56 | // Default classes to use when theming 57 | var DEFAULT_CLASSES = { 58 | tokenList: "token-input-list", 59 | token: "token-input-token", 60 | tokenDelete: "token-input-delete-token", 61 | selectedToken: "token-input-selected-token", 62 | highlightedToken: "token-input-highlighted-token", 63 | dropdown: "token-input-dropdown", 64 | dropdownItem: "token-input-dropdown-item", 65 | dropdownItem2: "token-input-dropdown-item2", 66 | selectedDropdownItem: "token-input-selected-dropdown-item", 67 | inputToken: "token-input-input-token" 68 | }; 69 | 70 | // Input box position "enum" 71 | var POSITION = { 72 | BEFORE: 0, 73 | AFTER: 1, 74 | END: 2 75 | }; 76 | 77 | // Keys "enum" 78 | var KEY = { 79 | BACKSPACE: 8, 80 | TAB: 9, 81 | ENTER: 13, 82 | ESCAPE: 27, 83 | SPACE: 32, 84 | PAGE_UP: 33, 85 | PAGE_DOWN: 34, 86 | END: 35, 87 | HOME: 36, 88 | LEFT: 37, 89 | UP: 38, 90 | RIGHT: 39, 91 | DOWN: 40, 92 | NUMPAD_ENTER: 108, 93 | COMMA: 188 94 | }; 95 | 96 | // Additional public (exposed) methods 97 | var methods = { 98 | init: function(url_or_data_or_function, options) { 99 | var settings = $.extend({}, DEFAULT_SETTINGS, options || {}); 100 | 101 | return this.each(function () { 102 | $(this).data("tokenInputObject", new $.TokenList(this, url_or_data_or_function, settings)); 103 | }); 104 | }, 105 | clear: function() { 106 | this.data("tokenInputObject").clear(); 107 | return this; 108 | }, 109 | add: function(item) { 110 | this.data("tokenInputObject").add(item); 111 | return this; 112 | }, 113 | remove: function(item) { 114 | this.data("tokenInputObject").remove(item); 115 | return this; 116 | }, 117 | get: function() { 118 | return this.data("tokenInputObject").getTokens(); 119 | } 120 | }; 121 | 122 | // Expose the .tokenInput function to jQuery as a plugin 123 | $.fn.tokenInput = function (method) { 124 | // Method calling and initialization logic 125 | if(methods[method]) { 126 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 127 | } else { 128 | return methods.init.apply(this, arguments); 129 | } 130 | }; 131 | 132 | // TokenList class for each input 133 | $.TokenList = function (input, url_or_data, settings) { 134 | // 135 | // Initialization 136 | // 137 | 138 | // Configure the data source 139 | if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") { 140 | // Set the url to query against 141 | settings.url = url_or_data; 142 | 143 | // If the URL is a function, evaluate it here to do our initalization work 144 | var url = computeURL(); 145 | 146 | // Make a smart guess about cross-domain if it wasn't explicitly specified 147 | if(settings.crossDomain === undefined) { 148 | if(url.indexOf("://") === -1) { 149 | settings.crossDomain = false; 150 | } else { 151 | settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); 152 | } 153 | } 154 | } else if(typeof(url_or_data) === "object") { 155 | // Set the local data to search through 156 | settings.local_data = url_or_data; 157 | } 158 | 159 | // Build class names 160 | if(settings.classes) { 161 | // Use custom class names 162 | settings.classes = $.extend({}, DEFAULT_CLASSES, settings.classes); 163 | } else if(settings.theme) { 164 | // Use theme-suffixed default class names 165 | settings.classes = {}; 166 | $.each(DEFAULT_CLASSES, function(key, value) { 167 | settings.classes[key] = value + "-" + settings.theme; 168 | }); 169 | } else { 170 | settings.classes = DEFAULT_CLASSES; 171 | } 172 | 173 | 174 | // Save the tokens 175 | var saved_tokens = []; 176 | 177 | // Keep track of the number of tokens in the list 178 | var token_count = 0; 179 | 180 | // Basic cache to save on db hits 181 | var cache = new $.TokenList.Cache(); 182 | 183 | // Keep track of the timeout, old vals 184 | var timeout; 185 | var input_val; 186 | 187 | // Create a new text input an attach keyup events 188 | var input_box = $("") 189 | .css({ 190 | outline: "none" 191 | }) 192 | .attr("id", settings.idPrefix + input.id) 193 | .focus(function () { 194 | if (settings.tokenLimit === null || settings.tokenLimit !== token_count) { 195 | show_dropdown_hint(); 196 | } 197 | }) 198 | .blur(function () { 199 | hide_dropdown(); 200 | $(this).val(""); 201 | }) 202 | .bind("keyup keydown blur update", resize_input) 203 | .keydown(function (event) { 204 | var previous_token; 205 | var next_token; 206 | 207 | switch(event.keyCode) { 208 | case KEY.LEFT: 209 | case KEY.RIGHT: 210 | case KEY.UP: 211 | case KEY.DOWN: 212 | if(!$(this).val()) { 213 | previous_token = input_token.prev(); 214 | next_token = input_token.next(); 215 | 216 | if((previous_token.length && previous_token.get(0) === selected_token) || (next_token.length && next_token.get(0) === selected_token)) { 217 | // Check if there is a previous/next token and it is selected 218 | if(event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) { 219 | deselect_token($(selected_token), POSITION.BEFORE); 220 | } else { 221 | deselect_token($(selected_token), POSITION.AFTER); 222 | } 223 | } else if((event.keyCode === KEY.LEFT || event.keyCode === KEY.UP) && previous_token.length) { 224 | // We are moving left, select the previous token if it exists 225 | select_token($(previous_token.get(0))); 226 | } else if((event.keyCode === KEY.RIGHT || event.keyCode === KEY.DOWN) && next_token.length) { 227 | // We are moving right, select the next token if it exists 228 | select_token($(next_token.get(0))); 229 | } 230 | } else { 231 | var dropdown_item = null; 232 | 233 | if(event.keyCode === KEY.DOWN || event.keyCode === KEY.RIGHT) { 234 | dropdown_item = $(selected_dropdown_item).next(); 235 | } else { 236 | dropdown_item = $(selected_dropdown_item).prev(); 237 | } 238 | 239 | if(dropdown_item.length) { 240 | select_dropdown_item(dropdown_item); 241 | } 242 | return false; 243 | } 244 | break; 245 | 246 | case KEY.BACKSPACE: 247 | previous_token = input_token.prev(); 248 | 249 | if(!$(this).val().length) { 250 | if(selected_token) { 251 | delete_token($(selected_token)); 252 | hidden_input.change(); 253 | } else if(previous_token.length) { 254 | select_token($(previous_token.get(0))); 255 | } 256 | 257 | return false; 258 | } else if($(this).val().length === 1) { 259 | hide_dropdown(); 260 | } else { 261 | // set a timeout just long enough to let this function finish. 262 | setTimeout(function(){do_search();}, 5); 263 | } 264 | break; 265 | 266 | case KEY.TAB: 267 | case KEY.ENTER: 268 | case KEY.NUMPAD_ENTER: 269 | case KEY.COMMA: 270 | if(selected_dropdown_item) { 271 | add_token($(selected_dropdown_item).data("tokeninput")); 272 | hidden_input.change(); 273 | return false; 274 | } 275 | break; 276 | 277 | case KEY.ESCAPE: 278 | hide_dropdown(); 279 | return true; 280 | 281 | default: 282 | if(String.fromCharCode(event.which)) { 283 | // set a timeout just long enough to let this function finish. 284 | setTimeout(function(){do_search();}, 5); 285 | } 286 | break; 287 | } 288 | }); 289 | 290 | // Keep a reference to the original input box 291 | var hidden_input = $(input) 292 | .hide() 293 | .val("") 294 | .focus(function () { 295 | input_box.focus(); 296 | }) 297 | .blur(function () { 298 | input_box.blur(); 299 | }); 300 | 301 | // Keep a reference to the selected token and dropdown item 302 | var selected_token = null; 303 | var selected_token_index = 0; 304 | var selected_dropdown_item = null; 305 | 306 | // The list to store the token items in 307 | var token_list = $("
      ") 308 | .addClass(settings.classes.tokenList) 309 | .click(function (event) { 310 | var li = $(event.target).closest("li"); 311 | if(li && li.get(0) && $.data(li.get(0), "tokeninput")) { 312 | toggle_select_token(li); 313 | } else { 314 | // Deselect selected token 315 | if(selected_token) { 316 | deselect_token($(selected_token), POSITION.END); 317 | } 318 | 319 | // Focus input box 320 | input_box.focus(); 321 | } 322 | }) 323 | .mouseover(function (event) { 324 | var li = $(event.target).closest("li"); 325 | if(li && selected_token !== this) { 326 | li.addClass(settings.classes.highlightedToken); 327 | } 328 | }) 329 | .mouseout(function (event) { 330 | var li = $(event.target).closest("li"); 331 | if(li && selected_token !== this) { 332 | li.removeClass(settings.classes.highlightedToken); 333 | } 334 | }) 335 | .insertBefore(hidden_input); 336 | 337 | // The token holding the input box 338 | var input_token = $("
    • ") 339 | .addClass(settings.classes.inputToken) 340 | .appendTo(token_list) 341 | .append(input_box); 342 | 343 | // The list to store the dropdown items in 344 | var dropdown = $("
      ") 345 | .addClass(settings.classes.dropdown) 346 | .appendTo("body") 347 | .hide(); 348 | 349 | // Magic element to help us resize the text input 350 | var input_resizer = $("") 351 | .insertAfter(input_box) 352 | .css({ 353 | position: "absolute", 354 | top: -9999, 355 | left: -9999, 356 | width: "auto", 357 | fontSize: input_box.css("fontSize"), 358 | fontFamily: input_box.css("fontFamily"), 359 | fontWeight: input_box.css("fontWeight"), 360 | letterSpacing: input_box.css("letterSpacing"), 361 | whiteSpace: "nowrap" 362 | }); 363 | 364 | // Pre-populate list if items exist 365 | hidden_input.val(""); 366 | var li_data = settings.prePopulate || hidden_input.data("pre"); 367 | if(settings.processPrePopulate && $.isFunction(settings.onResult)) { 368 | li_data = settings.onResult.call(hidden_input, li_data); 369 | } 370 | if(li_data && li_data.length) { 371 | $.each(li_data, function (index, value) { 372 | insert_token(value); 373 | checkTokenLimit(); 374 | }); 375 | } 376 | 377 | // Initialization is done 378 | if($.isFunction(settings.onReady)) { 379 | settings.onReady.call(); 380 | } 381 | 382 | // 383 | // Public functions 384 | // 385 | 386 | this.clear = function() { 387 | token_list.children("li").each(function() { 388 | if ($(this).children("input").length === 0) { 389 | delete_token($(this)); 390 | } 391 | }); 392 | } 393 | 394 | this.add = function(item) { 395 | add_token(item); 396 | } 397 | 398 | this.remove = function(item) { 399 | token_list.children("li").each(function() { 400 | if ($(this).children("input").length === 0) { 401 | var currToken = $(this).data("tokeninput"); 402 | var match = true; 403 | for (var prop in item) { 404 | if (item[prop] !== currToken[prop]) { 405 | match = false; 406 | break; 407 | } 408 | } 409 | if (match) { 410 | delete_token($(this)); 411 | } 412 | } 413 | }); 414 | } 415 | 416 | this.getTokens = function() { 417 | return saved_tokens; 418 | } 419 | 420 | // 421 | // Private functions 422 | // 423 | 424 | function checkTokenLimit() { 425 | if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { 426 | input_box.hide(); 427 | hide_dropdown(); 428 | return; 429 | } 430 | } 431 | 432 | function resize_input() { 433 | if(input_val === (input_val = input_box.val())) {return;} 434 | 435 | // Enter new content into resizer and resize input accordingly 436 | var escaped = input_val.replace(/&/g, '&').replace(/\s/g,' ').replace(//g, '>'); 437 | input_resizer.html(escaped); 438 | input_box.width(input_resizer.width() + 30); 439 | } 440 | 441 | function is_printable_character(keycode) { 442 | return ((keycode >= 48 && keycode <= 90) || // 0-1a-z 443 | (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * . 444 | (keycode >= 186 && keycode <= 192) || // ; = , - . / ^ 445 | (keycode >= 219 && keycode <= 222)); // ( \ ) ' 446 | } 447 | 448 | // Inner function to a token to the list 449 | function insert_token(item) { 450 | var this_token = settings.tokenFormatter(item); 451 | this_token = $(this_token) 452 | .addClass(settings.classes.token) 453 | .insertBefore(input_token); 454 | 455 | // The 'delete token' button 456 | $("" + settings.deleteText + "") 457 | .addClass(settings.classes.tokenDelete) 458 | .appendTo(this_token) 459 | .click(function () { 460 | delete_token($(this).parent()); 461 | hidden_input.change(); 462 | return false; 463 | }); 464 | 465 | // Store data on the token 466 | var token_data = {"id": item.id}; 467 | token_data[settings.propertyToSearch] = item[settings.propertyToSearch]; 468 | $.data(this_token.get(0), "tokeninput", item); 469 | 470 | // Save this token for duplicate checking 471 | saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index)); 472 | selected_token_index++; 473 | 474 | // Update the hidden input 475 | update_hidden_input(saved_tokens, hidden_input); 476 | 477 | token_count += 1; 478 | 479 | // Check the token limit 480 | if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { 481 | input_box.hide(); 482 | hide_dropdown(); 483 | } 484 | 485 | return this_token; 486 | } 487 | 488 | // Add a token to the token list based on user input 489 | function add_token (item) { 490 | var callback = settings.onAdd; 491 | 492 | // See if the token already exists and select it if we don't want duplicates 493 | if(token_count > 0 && settings.preventDuplicates) { 494 | var found_existing_token = null; 495 | token_list.children().each(function () { 496 | var existing_token = $(this); 497 | var existing_data = $.data(existing_token.get(0), "tokeninput"); 498 | if(existing_data && existing_data.id === item.id) { 499 | found_existing_token = existing_token; 500 | return false; 501 | } 502 | }); 503 | 504 | if(found_existing_token) { 505 | select_token(found_existing_token); 506 | //input_token.insertAfter(found_existing_token); //todo: Belsource: custom line for content tags 507 | input_box.focus(); 508 | return; 509 | } 510 | } 511 | 512 | // Insert the new tokens 513 | if(settings.tokenLimit == null || token_count < settings.tokenLimit) { 514 | insert_token(item); 515 | checkTokenLimit(); 516 | } 517 | 518 | // Clear input box 519 | input_box.val(""); 520 | 521 | // Don't show the help dropdown, they've got the idea 522 | hide_dropdown(); 523 | 524 | // Execute the onAdd callback if defined 525 | if($.isFunction(callback)) { 526 | callback.call(hidden_input,item); 527 | } 528 | } 529 | 530 | // Select a token in the token list 531 | function select_token (token) { 532 | token.addClass(settings.classes.selectedToken); 533 | selected_token = token.get(0); 534 | 535 | // Hide input box 536 | input_box.val(""); 537 | 538 | // Hide dropdown if it is visible (eg if we clicked to select token) 539 | hide_dropdown(); 540 | } 541 | 542 | // Deselect a token in the token list 543 | function deselect_token (token, position) { 544 | token.removeClass(settings.classes.selectedToken); 545 | selected_token = null; 546 | 547 | if(position === POSITION.BEFORE) { 548 | input_token.insertBefore(token); 549 | selected_token_index--; 550 | } else if(position === POSITION.AFTER) { 551 | input_token.insertAfter(token); 552 | selected_token_index++; 553 | } else { 554 | input_token.appendTo(token_list); 555 | selected_token_index = token_count; 556 | } 557 | 558 | // Show the input box and give it focus again 559 | input_box.focus(); 560 | } 561 | 562 | // Toggle selection of a token in the token list 563 | function toggle_select_token(token) { 564 | var previous_selected_token = selected_token; 565 | 566 | if(selected_token) { 567 | deselect_token($(selected_token), POSITION.END); 568 | } 569 | 570 | if(previous_selected_token === token.get(0)) { 571 | deselect_token(token, POSITION.END); 572 | } else { 573 | select_token(token); 574 | } 575 | } 576 | 577 | // Delete a token from the token list 578 | function delete_token (token) { 579 | // Remove the id from the saved list 580 | var token_data = $.data(token.get(0), "tokeninput"); 581 | var callback = settings.onDelete; 582 | 583 | var index = token.prevAll().length; 584 | if(index > selected_token_index) index--; 585 | 586 | // Delete the token 587 | token.remove(); 588 | selected_token = null; 589 | 590 | // Show the input box and give it focus again 591 | input_box.focus(); 592 | 593 | // Remove this token from the saved list 594 | saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1)); 595 | if(index < selected_token_index) selected_token_index--; 596 | 597 | // Update the hidden input 598 | update_hidden_input(saved_tokens, hidden_input); 599 | 600 | token_count -= 1; 601 | 602 | if(settings.tokenLimit !== null) { 603 | input_box 604 | .show() 605 | .val("") 606 | .focus(); 607 | } 608 | 609 | // Execute the onDelete callback if defined 610 | if($.isFunction(callback)) { 611 | callback.call(hidden_input,token_data); 612 | } 613 | } 614 | 615 | // Update the hidden input box value 616 | function update_hidden_input(saved_tokens, hidden_input) { 617 | var token_values = $.map(saved_tokens, function (el) { 618 | return el[settings.tokenValue]; 619 | }); 620 | hidden_input.val(token_values.join(settings.tokenDelimiter)); 621 | 622 | } 623 | 624 | // Hide and clear the results dropdown 625 | function hide_dropdown () { 626 | dropdown.hide().empty(); 627 | selected_dropdown_item = null; 628 | } 629 | 630 | function show_dropdown() { 631 | dropdown 632 | .css({ 633 | position: "absolute", 634 | top: $(token_list).offset().top + $(token_list).outerHeight(), 635 | left: $(token_list).offset().left, 636 | zindex: 999 637 | }) 638 | .show(); 639 | } 640 | 641 | function show_dropdown_searching () { 642 | if(settings.searchingText) { 643 | dropdown.html("

      "+settings.searchingText+"

      "); 644 | show_dropdown(); 645 | } 646 | } 647 | 648 | function show_dropdown_hint () { 649 | if(settings.hintText) { 650 | dropdown.html("

      "+settings.hintText+"

      "); 651 | show_dropdown(); 652 | } 653 | } 654 | 655 | // Highlight the query part of the search term 656 | function highlight_term(value, term) { 657 | return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); 658 | } 659 | 660 | function find_value_and_highlight_term(template, value, term) { 661 | return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term)); 662 | } 663 | 664 | // Populate the results dropdown with some results 665 | function populate_dropdown (query, results) { 666 | if(results && results.length) { 667 | dropdown.empty(); 668 | var dropdown_ul = $("
        ") 669 | .appendTo(dropdown) 670 | .mouseover(function (event) { 671 | select_dropdown_item($(event.target).closest("li")); 672 | }) 673 | .mousedown(function (event) { 674 | add_token($(event.target).closest("li").data("tokeninput")); 675 | hidden_input.change(); 676 | return false; 677 | }) 678 | .hide(); 679 | 680 | $.each(results, function(index, value) { 681 | var this_li = settings.resultsFormatter(value); 682 | 683 | this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query); 684 | 685 | this_li = $(this_li).appendTo(dropdown_ul); 686 | 687 | if(index % 2) { 688 | this_li.addClass(settings.classes.dropdownItem); 689 | } else { 690 | this_li.addClass(settings.classes.dropdownItem2); 691 | } 692 | 693 | if(index === 0) { 694 | select_dropdown_item(this_li); 695 | } 696 | 697 | $.data(this_li.get(0), "tokeninput", value); 698 | }); 699 | 700 | show_dropdown(); 701 | 702 | if(settings.animateDropdown) { 703 | dropdown_ul.slideDown("fast"); 704 | } else { 705 | dropdown_ul.show(); 706 | } 707 | } else { 708 | if(settings.noResultsText) { 709 | dropdown.html("

        "+settings.noResultsText+"

        "); 710 | show_dropdown(); 711 | } 712 | } 713 | } 714 | 715 | // Highlight an item in the results dropdown 716 | function select_dropdown_item (item) { 717 | if(item) { 718 | if(selected_dropdown_item) { 719 | deselect_dropdown_item($(selected_dropdown_item)); 720 | } 721 | 722 | item.addClass(settings.classes.selectedDropdownItem); 723 | selected_dropdown_item = item.get(0); 724 | } 725 | } 726 | 727 | // Remove highlighting from an item in the results dropdown 728 | function deselect_dropdown_item (item) { 729 | item.removeClass(settings.classes.selectedDropdownItem); 730 | selected_dropdown_item = null; 731 | } 732 | 733 | // Do a search and show the "searching" dropdown if the input is longer 734 | // than settings.minChars 735 | function do_search() { 736 | var query = input_box.val(); 737 | 738 | if(query && query.length) { 739 | if(selected_token) { 740 | deselect_token($(selected_token), POSITION.AFTER); 741 | } 742 | 743 | if(query.length >= settings.minChars) { 744 | show_dropdown_searching(); 745 | clearTimeout(timeout); 746 | 747 | timeout = setTimeout(function(){ 748 | run_search(query); 749 | }, settings.searchDelay); 750 | } else { 751 | hide_dropdown(); 752 | } 753 | } 754 | } 755 | 756 | // Do the actual search 757 | function run_search(query) { 758 | var cache_key = query + computeURL(); 759 | var cached_results = cache.get(cache_key); 760 | if(cached_results) { 761 | populate_dropdown(query, cached_results); 762 | } else { 763 | // Are we doing an ajax search or local data search? 764 | if(settings.url) { 765 | var url = computeURL(); 766 | // Extract exisiting get params 767 | var ajax_params = {}; 768 | ajax_params.data = {}; 769 | if(url.indexOf("?") > -1) { 770 | var parts = url.split("?"); 771 | ajax_params.url = parts[0]; 772 | 773 | var param_array = parts[1].split("&"); 774 | $.each(param_array, function (index, value) { 775 | var kv = value.split("="); 776 | ajax_params.data[kv[0]] = kv[1]; 777 | }); 778 | } else { 779 | ajax_params.url = url; 780 | } 781 | 782 | // Prepare the request 783 | ajax_params.data[settings.queryParam] = query; 784 | ajax_params.type = settings.method; 785 | ajax_params.dataType = settings.contentType; 786 | if(settings.crossDomain) { 787 | ajax_params.dataType = "jsonp"; 788 | } 789 | 790 | // Attach the success callback 791 | ajax_params.success = function(results) { 792 | if($.isFunction(settings.onResult)) { 793 | results = settings.onResult.call(hidden_input, results); 794 | } 795 | cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results); 796 | 797 | // only populate the dropdown if the results are associated with the active search query 798 | if(input_box.val().toLowerCase() === query.toLowerCase()) { 799 | populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results); 800 | } 801 | }; 802 | 803 | // Make the request 804 | $.ajax(ajax_params); 805 | } else if(settings.local_data) { 806 | // Do the search through local data 807 | var results = $.grep(settings.local_data, function (row) { 808 | //return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; 809 | return row[settings.propertyToSearch].indexOf(query.toLowerCase()) > -1; 810 | }); 811 | 812 | if($.isFunction(settings.onResult)) { 813 | results = settings.onResult.call(hidden_input, results); 814 | } 815 | cache.add(cache_key, results); 816 | populate_dropdown(query, results); 817 | } 818 | } 819 | } 820 | 821 | // compute the dynamic URL 822 | function computeURL() { 823 | var url = settings.url; 824 | if(typeof settings.url == 'function') { 825 | url = settings.url.call(); 826 | } 827 | return url; 828 | } 829 | }; 830 | 831 | // Really basic cache for the results 832 | $.TokenList.Cache = function (options) { 833 | var settings = $.extend({ 834 | max_size: 500 835 | }, options); 836 | 837 | var data = {}; 838 | var size = 0; 839 | 840 | var flush = function () { 841 | data = {}; 842 | size = 0; 843 | }; 844 | 845 | this.add = function (query, results) { 846 | if(size > settings.max_size) { 847 | flush(); 848 | } 849 | 850 | if(!data[query]) { 851 | size += 1; 852 | } 853 | 854 | data[query] = results; 855 | }; 856 | 857 | this.get = function (query) { 858 | return data[query]; 859 | }; 860 | }; 861 | }(jQuery)); 862 | -------------------------------------------------------------------------------- /resources/tokeninput/token-input-facebook.css: -------------------------------------------------------------------------------- 1 | /* Example tokeninput style #2: Facebook style */ 2 | ul.token-input-list-facebook { 3 | overflow: hidden; 4 | height: auto !important; 5 | height: 1%; 6 | width: 100%; 7 | border: 1px solid transparent; 8 | cursor: text; 9 | font-size: 12px; 10 | min-height: 1px; 11 | /*z-index: 1;*/ 12 | margin: 0; 13 | padding: 0; 14 | list-style-type: none; 15 | clear: left; 16 | } 17 | 18 | ul.token-input-list-facebook li input { 19 | width: 30px; 20 | padding: 1px 4px; 21 | background-color: #fafafa; 22 | border: 1px dashed #91bacf; 23 | margin: 3px; 24 | border-radius: 3px; 25 | -moz-border-radius: 3px; 26 | -webkit-border-radius: 3px; 27 | font-size: 12px; 28 | line-height: 16px; 29 | } 30 | 31 | ul.token-input-list-facebook li input:focus { 32 | background-color: #d8edf9; 33 | border: 1px solid #72b3d7; 34 | } 35 | 36 | li.token-input-token-facebook { 37 | overflow: hidden; 38 | height: 16px; 39 | margin: 3px; 40 | padding: 1px 4px; 41 | background-color: #eff2f7; 42 | color: #444; 43 | cursor: default; 44 | border: 1px solid #99C4DB; 45 | font-size: 12px; 46 | border-radius: 3px; 47 | -moz-border-radius: 3px; 48 | -webkit-border-radius: 3px; 49 | float: left; 50 | white-space: nowrap; 51 | } 52 | .token-input-token-facebook p{ 53 | font-size: 12px; 54 | line-height: 16px; 55 | } 56 | 57 | li.token-input-token-facebook p { 58 | display: inline; 59 | padding: 0; 60 | margin: 0; 61 | font-family: Arial; 62 | } 63 | 64 | li.token-input-token-facebook span { 65 | color: #007FC5; 66 | margin-left: 4px; 67 | font-weight: bold; 68 | cursor: pointer; 69 | font-family: arial; 70 | } 71 | 72 | li.token-input-selected-token-facebook { 73 | background-color: #007FC5; 74 | border: 1px solid #3b5998; 75 | color: #fff; 76 | } 77 | 78 | li.token-input-selected-token-facebook span { 79 | color: #fff; 80 | } 81 | 82 | li.token-input-input-token-facebook { 83 | float: left; 84 | margin: 0; 85 | padding: 0; 86 | list-style-type: none; 87 | } 88 | 89 | div.token-input-dropdown-facebook { 90 | position: absolute; 91 | width: 400px; 92 | background-color: #fff; 93 | overflow: hidden; 94 | border: 1px solid #b1b1b1; 95 | cursor: default; 96 | font-size: 12px; 97 | z-index: 10000 !important; 98 | border-radius: 3px; 99 | -moz-border-radius: 3px; 100 | -webkit-border-radius: 3px; 101 | } 102 | 103 | div.token-input-dropdown-facebook p { 104 | margin: 0; 105 | padding: 8px 5px; 106 | color: #007FC5; 107 | font-size: 12px; 108 | line-height: 12px; 109 | font-weight: bold; 110 | } 111 | 112 | div.token-input-dropdown-facebook ul { 113 | margin: 0; 114 | padding: 0; 115 | } 116 | 117 | div.token-input-dropdown-facebook ul li { 118 | padding: 5px; 119 | margin: 0; 120 | list-style-type: none; 121 | } 122 | 123 | div.token-input-dropdown-facebook ul li em { 124 | 125 | font-weight: bold; 126 | font-style: normal; 127 | } 128 | 129 | div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook { 130 | background-color: #007FC5; 131 | color: #fff; 132 | padding: 5px; 133 | } 134 | 135 | -------------------------------------------------------------------------------- /resources/tokeninput/token-input-mac.css: -------------------------------------------------------------------------------- 1 | /* Example tokeninput style #2: Mac Style */ 2 | fieldset.token-input-mac { 3 | position: relative; 4 | padding: 0; 5 | margin: 5px 0; 6 | background: #fff; 7 | width: 400px; 8 | border: 1px solid #A4BDEC; 9 | border-radius: 10px; 10 | -moz-border-radius: 10px; 11 | -webkit-border-radius: 10px; 12 | } 13 | 14 | fieldset.token-input-mac.token-input-dropdown-mac { 15 | border-radius: 10px 10px 0 0; 16 | -moz-border-radius: 10px 10px 0 0; 17 | -webkit-border-radius: 10px 10px 0 0; 18 | box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); 19 | -moz-box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); 20 | -webkit-box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); 21 | } 22 | 23 | ul.token-input-list-mac { 24 | overflow: hidden; 25 | height: auto !important; 26 | height: 1%; 27 | cursor: text; 28 | font-size: 12px; 29 | font-family: Verdana; 30 | min-height: 1px; 31 | z-index: 10000; 32 | margin: 0; 33 | padding: 3px; 34 | background: transparent; 35 | } 36 | 37 | ul.token-input-list-mac.error { 38 | border: 1px solid #C52020; 39 | } 40 | 41 | ul.token-input-list-mac li { 42 | list-style-type: none; 43 | } 44 | 45 | li.token-input-token-mac p { 46 | display: inline; 47 | padding: 0; 48 | margin: 0; 49 | } 50 | 51 | li.token-input-token-mac span { 52 | color: #a6b3cf; 53 | margin-left: 5px; 54 | font-weight: bold; 55 | cursor: pointer; 56 | } 57 | 58 | /* TOKENS */ 59 | 60 | li.token-input-token-mac { 61 | font-family: "Lucida Grande", Arial, serif; 62 | font-size: 9pt; 63 | line-height: 12pt; 64 | overflow: hidden; 65 | height: 16px; 66 | margin: 3px; 67 | padding: 0 10px; 68 | background: none; 69 | background-color: #dee7f8; 70 | color: #000; 71 | cursor: default; 72 | border: 1px solid #a4bdec; 73 | border-radius: 15px; 74 | -moz-border-radius: 15px; 75 | -webkit-border-radius: 15px; 76 | float: left; 77 | } 78 | 79 | li.token-input-highlighted-token-mac { 80 | background-color: #bbcef1; 81 | border: 1px solid #598bec; 82 | color: #000; 83 | } 84 | 85 | li.token-input-selected-token-mac { 86 | background-color: #598bec; 87 | border: 1px solid transparent; 88 | color: #fff; 89 | } 90 | 91 | li.token-input-highlighted-token-mac span.token-input-delete-token-mac { 92 | color: #000; 93 | } 94 | 95 | li.token-input-selected-token-mac span.token-input-delete-token-mac { 96 | color: #fff; 97 | } 98 | 99 | li.token-input-input-token-mac { 100 | border: none; 101 | background: transparent; 102 | float: left; 103 | padding: 0; 104 | margin: 0; 105 | } 106 | 107 | li.token-input-input-token-mac input { 108 | border: 0; 109 | width: 100px; 110 | padding: 3px; 111 | background-color: transparent; 112 | margin: 0; 113 | } 114 | 115 | div.token-input-dropdown-mac { 116 | position: absolute; 117 | border: 1px solid #A4BDEC; 118 | border-top: none; 119 | left: -1px; 120 | right: -1px; 121 | background-color: #fff; 122 | overflow: hidden; 123 | cursor: default; 124 | font-size: 10pt; 125 | font-family: "Lucida Grande", Arial, serif; 126 | padding: 5px; 127 | border-radius: 0 0 10px 10px; 128 | -moz-border-radius: 0 0 10px 10px; 129 | -webkit-border-radius: 0 0 10px 10px; 130 | box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); 131 | -moz-box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); 132 | -webkit-box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25); 133 | clip:rect(0px, 1000px, 1000px, -10px); 134 | } 135 | 136 | div.token-input-dropdown-mac p { 137 | font-size: 8pt; 138 | margin: 0; 139 | padding: 0 5px; 140 | font-style: italic; 141 | color: #aaa; 142 | } 143 | 144 | div.token-input-dropdown-mac h3.token-input-dropdown-category-mac { 145 | font-family: "Lucida Grande", Arial, serif; 146 | font-size: 10pt; 147 | font-weight: bold; 148 | border: none; 149 | padding: 0 5px; 150 | margin: 0; 151 | } 152 | 153 | div.token-input-dropdown-mac ul { 154 | margin: 0; 155 | padding: 0; 156 | } 157 | 158 | div.token-input-dropdown-mac ul li { 159 | list-style-type: none; 160 | cursor: pointer; 161 | background: none; 162 | background-color: #fff; 163 | margin: 0; 164 | padding: 0 0 0 25px; 165 | } 166 | 167 | div.token-input-dropdown-mac ul li.token-input-dropdown-item-mac { 168 | background-color: #fff; 169 | } 170 | 171 | div.token-input-dropdown-mac ul li.token-input-dropdown-item-mac.odd { 172 | background-color: #ECF4F9; 173 | border-radius: 15px; 174 | -moz-border-radius: 15px; 175 | -webkit-border-radius: 15px; 176 | } 177 | 178 | div.token-input-dropdown-mac ul li.token-input-dropdown-item-mac span.token-input-dropdown-item-description-mac { 179 | float: right; 180 | font-size: 8pt; 181 | font-style: italic; 182 | padding: 0 10px 0 0; 183 | color: #999; 184 | } 185 | 186 | div.token-input-dropdown-mac ul li strong { 187 | font-weight: bold; 188 | text-decoration: underline; 189 | font-style: none; 190 | } 191 | 192 | div.token-input-dropdown-mac ul li.token-input-selected-dropdown-item-mac, 193 | div.token-input-dropdown-mac ul li.token-input-selected-dropdown-item-mac.odd { 194 | background-color: #598bec; 195 | color: #fff; 196 | border-radius: 15px; 197 | -moz-border-radius: 15px; 198 | -webkit-border-radius: 15px; 199 | } 200 | 201 | div.token-input-dropdown-mac ul li.token-input-selected-dropdown-item-mac span.token-input-dropdown-item-description-mac, 202 | div.token-input-dropdown-mac ul li.token-input-selected-dropdown-item-mac.odd span.token-input-dropdown-item-description-mac { 203 | color: #fff; 204 | } 205 | -------------------------------------------------------------------------------- /resources/tokeninput/token-input.css: -------------------------------------------------------------------------------- 1 | /* Example tokeninput style #1: Token vertical list*/ 2 | ul.token-input-list { 3 | overflow: hidden; 4 | height: auto !important; 5 | height: 1%; 6 | width: 400px; 7 | border: 1px solid #999; 8 | cursor: text; 9 | font-size: 12px; 10 | font-family: Verdana; 11 | z-index: 10000; 12 | margin: 0; 13 | padding: 0; 14 | background-color: #fff; 15 | list-style-type: none; 16 | clear: left; 17 | } 18 | 19 | ul.token-input-list li { 20 | list-style-type: none; 21 | } 22 | 23 | ul.token-input-list li input { 24 | border: 0; 25 | width: 350px; 26 | padding: 3px 8px; 27 | background-color: white; 28 | -webkit-appearance: caret; 29 | } 30 | 31 | li.token-input-token { 32 | overflow: hidden; 33 | height: auto !important; 34 | height: 1%; 35 | margin: 3px; 36 | padding: 3px 5px; 37 | background-color: #d0efa0; 38 | color: #000; 39 | font-weight: bold; 40 | cursor: default; 41 | display: block; 42 | } 43 | 44 | li.token-input-token p { 45 | float: left; 46 | padding: 0; 47 | margin: 0; 48 | } 49 | 50 | li.token-input-token span { 51 | float: right; 52 | color: #777; 53 | cursor: pointer; 54 | } 55 | 56 | li.token-input-selected-token { 57 | background-color: #08844e; 58 | color: #fff; 59 | } 60 | 61 | li.token-input-selected-token span { 62 | color: #bbb; 63 | } 64 | 65 | div.token-input-dropdown { 66 | position: absolute; 67 | width: 400px; 68 | background-color: #fff; 69 | overflow: hidden; 70 | border-left: 1px solid #ccc; 71 | border-right: 1px solid #ccc; 72 | border-bottom: 1px solid #ccc; 73 | cursor: default; 74 | font-size: 12px; 75 | font-family: Verdana; 76 | z-index: 10000; 77 | } 78 | 79 | div.token-input-dropdown p { 80 | margin: 0; 81 | padding: 5px; 82 | font-weight: bold; 83 | color: #777; 84 | } 85 | 86 | div.token-input-dropdown ul { 87 | margin: 0; 88 | padding: 0; 89 | } 90 | 91 | div.token-input-dropdown ul li { 92 | background-color: #fff; 93 | padding: 3px; 94 | list-style-type: none; 95 | } 96 | 97 | div.token-input-dropdown ul li.token-input-dropdown-item { 98 | background-color: #fafafa; 99 | } 100 | 101 | div.token-input-dropdown ul li.token-input-dropdown-item2 { 102 | background-color: #fff; 103 | } 104 | 105 | div.token-input-dropdown ul li em { 106 | font-weight: bold; 107 | font-style: normal; 108 | } 109 | 110 | div.token-input-dropdown ul li.token-input-selected-dropdown-item { 111 | background-color: #d0efa0; 112 | } 113 | 114 | --------------------------------------------------------------------------------