├── .gitignore ├── README.md ├── config.schema.json ├── index.js ├── normensTemplates.template ├── package.json ├── tc-serial.js ├── tc-websocket.js └── websocket-client.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .homebridge/ 3 | build 4 | package-lock.json 5 | .tern-project 6 | tags 7 | session.vim 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DISCONTINUED 2 | 3 | As I moved my whole setup to a [FHEM](https://fhem.de) server with a [SIGNALduino](https://github.com/RFD-FHEM/SIGNALDuino) this project is now deprecated. This does *not* mean that its not working or will stop working in the future. 4 | 5 | The good news is that if you want to change your existing setup to a SIGNALDuino as well you probably already have all the hardware to do that. 6 | 7 | If you are using the CC1101 sender theres a good chance you can make a SIGNALDuino from that hardware! Check the [arduino-433](https://github.com/normen/arduino-433) project for instructions. 8 | 9 | ## Migration to FHEM 10 | 11 | - Use max cube or build/buy a CUL 868MHz stick 12 | - Install [FHEM](https://fhem.de) on server 13 | - Install [fhem plugin](https://github.com/justme-1968/homebridge-fhem) in homebridge 14 | - Define siri in fhem 15 | - Define SIGNALduino in fhem 16 | - Discover devices, apply fixes below if needed 17 | - Set siriName for devices that should appear in HomeBridge 18 | - Optionally set homebridgeMapping for special devices 19 | - Profit 20 | 21 | #### AttrTemplates 22 | 23 | To ease the setup of some switches I created so called `attrTemplate` files to quickly apply settings to a discovered switch. Some switches needed some massaging to work correctly. 24 | 25 | Put [this file](normensTemplates.template) in your `/opt/fhem/FHEM/lib/AttrTemplate` folder, then apply the templates through the FHEMWEB UI `set` command or otherwise. 26 | 27 | - `433_Clarus_fix` 28 | - Fix "Clarus" switches (cheap socket switches) 29 | - Fixes wrong on/off codes ("Code 01 Unknown") 30 | - Adapts pulse frequency so switches react 31 | - `433_Intertechno_fix` 32 | - Fix "Intertechno" switches (especially old) 33 | - Adapts pulse frequency so switches react better 34 | - `Add_Switches` 35 | - Add "Buttons" for existing switches on/off presses 36 | - `Make_SmokeDetect` 37 | - Makes a smoke detector from IT devices that are recognized as a switch 38 | - Adds a watchdog that resets the "on" state automatically 39 | - `Make_LeakDetect` 40 | - Makes a leak detector from IT devices that are recognized as a switch 41 | - Adds a watchdog that resets the "on" state automatically 42 | - `Make_MotionSensor` 43 | - Makes a motion detector from IT devices that are recognized as a switch 44 | - Adds a watchdog that resets the "on" state automatically 45 | 46 | 47 | # homebridge-433-arduino 48 | [![NPM Version](https://img.shields.io/npm/v/homebridge-433-arduino.svg)](https://www.npmjs.com/package/homebridge-433-arduino) [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 49 | 50 | A homebridge plugin to control 433MHz switches and receive 433MHz switch signals using an Arduino Micro or an ESP8266 / ESP32 connected via USB or WiFi to send and receive data. 51 | 52 | ## Introduction 53 | This plugin allows you to use cheap 433MHz wireless switches as lamps, fans or generic switches in HomeKit and control them using Siri. You can also use your 433Mhz remote to control things in HomeKit, like for example start scenes. 54 | 55 | ### Improvements over other similar plugins 56 | - Bidirectional, can send and receive switch signals 57 | - Virtually no CPU load on the server (RasPi) even when receiving 58 | - Sending signals works properly and sequentially, no broken signals when many devices are controlled 59 | - Rock-solid RF signal quality and timing through external micro controller 60 | - Transceiver can use WiFi so it doesn't need a physical connection to the homebridge server 61 | - Supports homebridge-config-ui-x to set up switches via web interface 62 | 63 | ### Why use an external microcontroller? 64 | There is plugins out there that use the Raspberry Pi GPIO functions to send and receive 433 MHZ data. The problem with these is that especially the receiving part requires quite a lot of CPU power as the RasPi lacks real hardware interrupts on its GPIO ports. Sending works okay most of the time if the RasPi isn't under much load. The RasPi 1 can struggle to get accurate RF timing with short pulse durations even under low load however. 65 | 66 | Additionally, the RasPi works on 3.3V and most simple 433MHz receivers/transmitters work best at 5V. The Arduino micro for example runs on 5V and allows a much more stable connection to the receivers and transmitters. 67 | 68 | ### Supported switches 69 | Most cheap 433 MHz switches should work, the transceiver can use either rc-switch or ESPiLight to encode and decode signals. ESPiLight is recommended as it supports more switch types but as the name suggests it requires ESP hardware. 70 | 71 | ## Installation 72 | ### Hardware 73 | The software for the microcontroller has it's own github project. For info on how to set up the microcontroller hardware, see this page: https://github.com/normen/arduino-433 74 | 75 | ### Homebridge 76 | #### Install software 77 | 1. Install this plugin using: `npm install --unsafe-perm -g homebridge-433-arduino` 78 | 2. Update your configuration file. See the sample below. 79 | 3. Optionally if you have homebridge-config-ui-x installed use the settings panel to configure the plugin 80 | 81 | #### Configure config.json 82 | ##### Example config.json 83 | ```javascript 84 | { 85 | "bridge": { 86 | "name": "#####", 87 | "username": "", 88 | "port": 51826, 89 | "pin": "" 90 | }, 91 | 92 | "platforms": [ 93 | { 94 | "platform": "ArduinoRCSwitch", 95 | "name": "Arduino RC Switch Platform", 96 | "serial_port": "/dev/ttyACM0", 97 | "switches": [ 98 | { 99 | "name" : "My Device", 100 | "on": { 101 | "code":123456, 102 | "pulse":188 103 | }, 104 | "off": { 105 | "code":123457, 106 | "pulse":188 107 | } 108 | }, 109 | { 110 | "name" : "My Other Device", 111 | "on": { 112 | "code":123458, 113 | "pulse":188, 114 | "protocol":2 115 | }, 116 | "off": { 117 | "code":123459, 118 | "pulse":188, 119 | "protocol":2 120 | } 121 | } 122 | ], 123 | "buttons": [ 124 | { 125 | "name" : "My Button", 126 | "code":123450, 127 | "pulse":188 128 | } 129 | ], 130 | "detectors": [ 131 | { 132 | "name" : "My Smoke Detector", 133 | "code":1234502, 134 | "pulse":366 135 | } 136 | ], 137 | "sensors": [ 138 | { 139 | "name" : "My Leak Sensor", 140 | "code":1234503, 141 | "pulse":366 142 | } 143 | ] 144 | } 145 | ] 146 | } 147 | 148 | ``` 149 | ##### Settings 150 | - `serial_port` is the USB port you have your Arduino connected to, normally /dev/ttyACM0 (Arduino) or /dev/ttyUSB0 (ESP) on Raspberry Pi. To find the right port do a `ls /dev/tty*`, then connect the transceiver and do `ls /dev/tty*` again, the newly added port is the right one. 151 | 152 | - `switches` is the list of configured switches. When Homebridge is running the console will show the needed code and pulse values for any received 433MHz signals it can decode so you can find them there and enter them in your config.json file. Switches work bidirectionally, when a switch is changed in homekit a 433 signal is sent, when the 433 signal is received the switch in homekit is changed. 153 | 154 | - `buttons` is a list of configured buttons. Buttons work differently in that there is no on/off pair, each signal is routed to its own switch. These switches enable for one second and then disable again. This makes it easy to trigger scenes with these buttons regardless of their on/off state. 155 | Buttons only work for receiving signals. 156 | 157 | - `detectors` is a list of configured smoke detectors. Smoke detectors will only report their current state (smoke detected or not). 158 | 159 | - `sensors` is a list of configured leak sensors. Leak sensors will only report their current state (leak detected or not). 160 | 161 | - `motion` is a list of configured motion sensors. Motion sensors will only report their current state (motion detected or not). 162 | 163 | ##### Optional settings 164 | - `host` is the hostname of the WiFi transceiver, not used when serial_port is given. When running on ESP hardware the library can optionally use WiFi / websockets instead of a serial port to connect to the transceiver. 165 | 166 | - `port` is the port of the WiFi transceiver, not used when serial_port is given 167 | 168 | - `input_output_timeout` is the time in milliseconds that the plugin waits after it has received a signal before sending any signals itself. This is to avoid interfering with switches that send signals. If both the Arduino and the switch are sending at the same time none of the signals will be decoded by the receivers. The default value is `100`. 169 | You will only need this value if you have 433 switches that control scenes which in turn control 433 plugs. In that case the switch is sending 433 signals and if the plugin would start sending immediately when it decodes the first signal it might start sending while the switch is still sending as well, mixing the signals. 170 | Decrease this value to get quicker response of 433 plugs in the aforementioned scenarios, increase it if 433 plugs don't react at all in such scenarios. 171 | 172 | - `throttle` is the time in milliseconds that the incoming signal of a single button or switch will be throttled. This is to avoid switches triggering HomeKit multiple times when pressed. The default value is `500`. 173 | 174 | ##### ESPiLight 175 | Optionally you can use the ESPiLight library instead of rc-switch on the transceiver which supports a wider range of 433MHz devices. When using it (configured in the Arduino code) the format of the messages changes from code/pulse/protocol to type and message (different for each switch type), see below for an example. 176 | 177 | Note that for some switches not all of the received info that is given in the homebridge log needs to be added to the config.json. Usually "id", "unit" and "state" are enough. 178 | 179 | ##### Example config.json with Websockets & ESPPiLight 180 | For switches that report "up" and "down" for the state instead of "on" and "off" you can specify "state":"up" in the configuration to account for that. For buttons you have to specify which state you want to use as a button. 181 | 182 | ```javascript 183 | "platforms": [ 184 | { 185 | "platform": "ArduinoRCSwitch", 186 | "name": "Arduino RC Switch Platform", 187 | "host": "arduino-433", 188 | "port": 80, 189 | "switches": [ 190 | { 191 | "name" : "My Device", 192 | "type": "clarus_switch", 193 | "message":{ 194 | "id": "A3", 195 | "unit": 60 196 | } 197 | } 198 | ], 199 | "buttons": [ 200 | { 201 | "name" : "My Button", 202 | "type": "clarus_switch", 203 | "message":{ 204 | "id": "B4", 205 | "unit": 20, 206 | "state": "off" 207 | } 208 | } 209 | ] 210 | } 211 | ] 212 | ``` 213 | 214 | ## Usage 215 | ### Adding Switches 216 | To add switches press a button on the remote control that came with the switch and watch the homebridge log. Switch messages should appear in the log, giving you the needed information to fill out config.json or add switches through the web interface settings panel. 217 | 218 | See the wiki for more info as well as tips&tricks for getting your switches to work. Also please add your own info about switches. 219 | 220 | https://github.com/normen/homebridge-433-arduino/wiki 221 | 222 | ## Development 223 | If you want new features or improve the plugin, you're very welcome to do so. The projects `devDependencies` include homebridge and the `npm run test` command has been adapted so that you can run a test instance of homebridge during development. 224 | #### Setup 225 | - clone github repo 226 | - `npm install` in the project folder 227 | - create `.homebridge` folder in project root 228 | - add `config.json` with appropriate content to `.homebridge` folder 229 | - run `npm run test` to start the homebridge instance for testing 230 | 231 | ## Credits 232 | 233 | Credit goes to 234 | - rainlake (https://github.com/rainlake/homebridge-platform-rcswitch) 235 | 236 | ## License 237 | 238 | Published under the MIT License. 239 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "ArduinoRCSwitch", 3 | "pluginType": "platform", 4 | "headerDisplay": "For setting up the USB/WiFi transceiver see https://github.com/normen/arduino-433", 5 | "footerDisplay": "For help please see https://github.com/normen/homebridge-433-arduino/wiki", 6 | "schema": { 7 | "name": { 8 | "title": "Name", 9 | "type": "string", 10 | "default": "Arduino RC Switch Platform", 11 | "required": true 12 | }, 13 | "use_espilight": { 14 | "title": "ESPiLight Mode", 15 | "description": "Enable this if you have ESPiLight mode enabled on your arduino-433 transceiver", 16 | "type": "boolean", 17 | "required": false 18 | }, 19 | "serial_port": { 20 | "title": "Serial Port Name", 21 | "description": "e.g. '/dev/ttyUSB0'", 22 | "type": "string", 23 | "required": false 24 | }, 25 | "host": { 26 | "title": "Websocket Server IP or Hostname", 27 | "description": "e.g. 'arduino-433' - remove the serial port entry above to use WebSocket", 28 | "type": "string", 29 | "required": false 30 | }, 31 | "port": { 32 | "title": "Websocket Port Number", 33 | "description": "usually not needed", 34 | "type": "number", 35 | "required": false 36 | }, 37 | "input_output_timeout": { 38 | "title": "I/O Timeout", 39 | "description": "radio silence time after receiving to avoid mixing signals, defaults to 100(ms)", 40 | "type": "number", 41 | "required": false 42 | }, 43 | "throttle": { 44 | "title": "Input Message Throttle", 45 | "description": "throttle input messages to not trigger homekit too often if buttons send a lot of signals, default 500(ms)", 46 | "type": "number", 47 | "required": false 48 | }, 49 | "switches": { 50 | "title": "On/Off Switches", 51 | "required": false, 52 | "type": "array", 53 | "items": { 54 | "name": "Switch Config", 55 | "type": "object", 56 | "properties": { 57 | "name": { 58 | "title": "Name", 59 | "type": "string", 60 | "required": true 61 | }, 62 | "hint": { 63 | "title": "Hint", 64 | "type": "string", 65 | "description": "Your own hint to remember the switch", 66 | "required": false 67 | }, 68 | "type": { 69 | "title": "Type", 70 | "type": "string", 71 | "required": false 72 | }, 73 | "message": { 74 | "title": "Message", 75 | "type": "object", 76 | "required": false, 77 | "properties":{ 78 | "id":{ 79 | "title":"ID", 80 | "type":"string", 81 | "required": false 82 | }, 83 | "unit":{ 84 | "title":"Unit", 85 | "type":"string", 86 | "required": false 87 | }, 88 | "state":{ 89 | "title":"State", 90 | "type":"string", 91 | "required": false 92 | } 93 | } 94 | }, 95 | "on":{ 96 | "title": "On Message", 97 | "type": "object", 98 | "required": false, 99 | "properties":{ 100 | "code":{ 101 | "title":"Code", 102 | "type":"number", 103 | "required":false 104 | }, 105 | "pulse":{ 106 | "title":"Pulse", 107 | "type":"number", 108 | "required":false 109 | }, 110 | "protocol":{ 111 | "title":"Protocol", 112 | "type":"number", 113 | "required":false 114 | } 115 | } 116 | }, 117 | "off":{ 118 | "title": "Off Message", 119 | "type": "object", 120 | "required": false, 121 | "properties":{ 122 | "code":{ 123 | "title":"Code", 124 | "type":"number", 125 | "required":false 126 | }, 127 | "pulse":{ 128 | "title":"Pulse", 129 | "type":"number", 130 | "required":false 131 | }, 132 | "protocol":{ 133 | "title":"Protocol", 134 | "type":"number", 135 | "required":false 136 | } 137 | } 138 | } 139 | } 140 | } 141 | }, 142 | "buttons": { 143 | "title": "Buttons", 144 | "required": false, 145 | "type": "array", 146 | "items": { 147 | "name": "Button Config", 148 | "type": "object", 149 | "properties": { 150 | "name": { 151 | "title": "Name", 152 | "type": "string", 153 | "required": true 154 | }, 155 | "hint": { 156 | "title": "Hint", 157 | "type": "string", 158 | "description": "Your own hint to remember the button", 159 | "required": false 160 | }, 161 | "type": { 162 | "title": "Type", 163 | "type": "string", 164 | "required": false 165 | }, 166 | "message": { 167 | "title": "Message", 168 | "type": "object", 169 | "required": false, 170 | "properties":{ 171 | "id":{ 172 | "title":"ID", 173 | "type":"string", 174 | "required": false 175 | }, 176 | "unit":{ 177 | "title":"Unit", 178 | "type":"string", 179 | "required": false 180 | }, 181 | "state":{ 182 | "title":"State", 183 | "type":"string", 184 | "required": false 185 | } 186 | } 187 | }, 188 | "code":{ 189 | "title":"Code", 190 | "type":"number", 191 | "required":false 192 | }, 193 | "pulse":{ 194 | "title":"Pulse", 195 | "type":"number", 196 | "required":false 197 | }, 198 | "protocol":{ 199 | "title":"Protocol", 200 | "type":"number", 201 | "required":false 202 | } 203 | } 204 | } 205 | }, 206 | "sensors": { 207 | "title": "Water Sensors", 208 | "required": false, 209 | "type": "array", 210 | "items": { 211 | "name": "Sensor Config", 212 | "type": "object", 213 | "properties": { 214 | "name": { 215 | "title": "Name", 216 | "type": "string", 217 | "required": true 218 | }, 219 | "hint": { 220 | "title": "Hint", 221 | "type": "string", 222 | "description": "Your own hint to remember the sensor", 223 | "required": false 224 | }, 225 | "type": { 226 | "title": "Type", 227 | "type": "string", 228 | "required": false 229 | }, 230 | "message": { 231 | "title": "Message", 232 | "type": "object", 233 | "required": false, 234 | "properties":{ 235 | "id":{ 236 | "title":"ID", 237 | "type":"string", 238 | "required": false 239 | }, 240 | "unit":{ 241 | "title":"Unit", 242 | "type":"string", 243 | "required": false 244 | }, 245 | "state":{ 246 | "title":"State", 247 | "type":"string", 248 | "required": false 249 | } 250 | } 251 | }, 252 | "code":{ 253 | "title":"Code", 254 | "type":"number", 255 | "required":false 256 | }, 257 | "pulse":{ 258 | "title":"Pulse", 259 | "type":"number", 260 | "required":false 261 | }, 262 | "protocol":{ 263 | "title":"Protocol", 264 | "type":"number", 265 | "required":false 266 | } 267 | } 268 | } 269 | }, 270 | "detectors": { 271 | "title": "Smoke Detectors", 272 | "required": false, 273 | "type": "array", 274 | "items": { 275 | "name": "Detector Config", 276 | "type": "object", 277 | "properties": { 278 | "name": { 279 | "title": "Name", 280 | "type": "string", 281 | "required": true 282 | }, 283 | "hint": { 284 | "title": "Hint", 285 | "type": "string", 286 | "description": "Your own hint to remember the detector", 287 | "required": false 288 | }, 289 | "type": { 290 | "title": "Type", 291 | "type": "string", 292 | "required": false 293 | }, 294 | "message": { 295 | "title": "Message", 296 | "type": "object", 297 | "required": false, 298 | "properties":{ 299 | "id":{ 300 | "title":"ID", 301 | "type":"string", 302 | "required": false 303 | }, 304 | "unit":{ 305 | "title":"Unit", 306 | "type":"string", 307 | "required": false 308 | }, 309 | "state":{ 310 | "title":"State", 311 | "type":"string", 312 | "required": false 313 | } 314 | } 315 | }, 316 | "code":{ 317 | "title":"Code", 318 | "type":"number", 319 | "required":false 320 | }, 321 | "pulse":{ 322 | "title":"Pulse", 323 | "type":"number", 324 | "required":false 325 | }, 326 | "protocol":{ 327 | "title":"Protocol", 328 | "type":"number", 329 | "required":false 330 | } 331 | } 332 | } 333 | }, 334 | "motion": { 335 | "title": "Motion Detectors", 336 | "required": false, 337 | "type": "array", 338 | "items": { 339 | "name": "Detector Config", 340 | "type": "object", 341 | "properties": { 342 | "name": { 343 | "title": "Name", 344 | "type": "string", 345 | "required": true 346 | }, 347 | "hint": { 348 | "title": "Hint", 349 | "type": "string", 350 | "description": "Your own hint to remember the detector", 351 | "required": false 352 | }, 353 | "type": { 354 | "title": "Type", 355 | "type": "string", 356 | "required": false 357 | }, 358 | "message": { 359 | "title": "Message", 360 | "type": "object", 361 | "required": false, 362 | "properties":{ 363 | "id":{ 364 | "title":"ID", 365 | "type":"string", 366 | "required": false 367 | }, 368 | "unit":{ 369 | "title":"Unit", 370 | "type":"string", 371 | "required": false 372 | }, 373 | "state":{ 374 | "title":"State", 375 | "type":"string", 376 | "required": false 377 | } 378 | } 379 | }, 380 | "code":{ 381 | "title":"Code", 382 | "type":"number", 383 | "required":false 384 | }, 385 | "pulse":{ 386 | "title":"Pulse", 387 | "type":"number", 388 | "required":false 389 | }, 390 | "protocol":{ 391 | "title":"Protocol", 392 | "type":"number", 393 | "required":false 394 | } 395 | } 396 | } 397 | } 398 | }, 399 | "form": [ 400 | "name", 401 | "serial_port", 402 | { 403 | "type": "section", 404 | "expandable": true, 405 | "expanded": false, 406 | "title": "WiFi / WebSocket", 407 | "items": [ 408 | "host", 409 | "port" 410 | ] 411 | }, 412 | { 413 | "type": "section", 414 | "expandable": true, 415 | "expanded": false, 416 | "title": "Advanced", 417 | "items": [ 418 | "input_output_timeout", 419 | "throttle" 420 | ] 421 | }, 422 | "use_espilight", 423 | { 424 | "type": "fieldset", 425 | "expandable": true, 426 | "expanded": false, 427 | "title": "On/Off Switches", 428 | "items": [ 429 | { 430 | "key": "switches", 431 | "items":[ 432 | "switches[].name", 433 | "switches[].hint", 434 | { 435 | "key": "switches[].type", 436 | "condition": { "functionBody": "return model.use_espilight" } 437 | }, 438 | { 439 | "key": "switches[].message", 440 | "condition": { "functionBody": "return model.use_espilight" }, 441 | "items":[ 442 | { 443 | "type": "flex", 444 | "flex-flow": "row wrap", 445 | "items": [ 446 | "switches[].message.id", 447 | "switches[].message.unit" 448 | ] 449 | } 450 | ] 451 | }, 452 | { 453 | "key": "switches[].on", 454 | "condition": { "functionBody": "return !model.use_espilight" }, 455 | "items":[ 456 | { 457 | "type": "flex", 458 | "flex-flow": "row wrap", 459 | "items": [ 460 | "switches[].on.code", 461 | "switches[].on.pulse", 462 | "switches[].on.protocol" 463 | ] 464 | } 465 | ] 466 | }, 467 | { 468 | "key": "switches[].off", 469 | "type": "object", 470 | "condition": { "functionBody": "return !model.use_espilight" }, 471 | "items":[ 472 | { 473 | "type": "flex", 474 | "flex-flow": "row wrap", 475 | "items": [ 476 | "switches[].off.code", 477 | "switches[].off.pulse", 478 | "switches[].off.protocol" 479 | ] 480 | } 481 | ] 482 | } 483 | ] 484 | } 485 | ] 486 | }, 487 | { 488 | "type": "fieldset", 489 | "expandable": true, 490 | "expanded": false, 491 | "title": "Buttons", 492 | "items": [ 493 | { 494 | "key": "buttons", 495 | "items":[ 496 | "buttons[].name", 497 | "buttons[].hint", 498 | { 499 | "key": "buttons[].code", 500 | "condition": { "functionBody": "return !model.use_espilight" } 501 | }, 502 | { 503 | "key": "buttons[].pulse", 504 | "condition": { "functionBody": "return !model.use_espilight" } 505 | }, 506 | { 507 | "key": "buttons[].protocol", 508 | "condition": { "functionBody": "return !model.use_espilight" } 509 | }, 510 | { 511 | "key": "buttons[].type", 512 | "condition": { "functionBody": "return model.use_espilight" } 513 | }, 514 | { 515 | "key": "buttons[].message", 516 | "condition": { "functionBody": "return model.use_espilight" }, 517 | "items":[ 518 | { 519 | "type": "flex", 520 | "flex-flow": "row wrap", 521 | "items": [ 522 | "buttons[].message.id", 523 | "buttons[].message.unit", 524 | "buttons[].message.state" 525 | ] 526 | } 527 | ] 528 | } 529 | ] 530 | } 531 | ] 532 | }, 533 | { 534 | "type": "fieldset", 535 | "expandable": true, 536 | "expanded": false, 537 | "title": "Water Sensors", 538 | "items": [ 539 | { 540 | "key": "sensors", 541 | "items":[ 542 | "sensors[].name", 543 | "sensors[].hint", 544 | { 545 | "key": "sensors[].code", 546 | "condition": { "functionBody": "return !model.use_espilight" } 547 | }, 548 | { 549 | "key": "sensors[].pulse", 550 | "condition": { "functionBody": "return !model.use_espilight" } 551 | }, 552 | { 553 | "key": "sensors[].protocol", 554 | "condition": { "functionBody": "return !model.use_espilight" } 555 | }, 556 | { 557 | "key": "sensors[].type", 558 | "condition": { "functionBody": "return model.use_espilight" } 559 | }, 560 | { 561 | "key": "sensors[].message", 562 | "condition": { "functionBody": "return model.use_espilight" }, 563 | "items":[ 564 | { 565 | "type": "flex", 566 | "flex-flow": "row wrap", 567 | "items": [ 568 | "sensors[].message.id", 569 | "sensors[].message.unit", 570 | "sensors[].message.state" 571 | ] 572 | } 573 | ] 574 | } 575 | ] 576 | } 577 | ] 578 | }, 579 | { 580 | "type": "fieldset", 581 | "expandable": true, 582 | "expanded": false, 583 | "title": "Smoke Detectors", 584 | "items": [ 585 | { 586 | "key": "detectors", 587 | "items":[ 588 | "detectors[].name", 589 | "detectors[].hint", 590 | { 591 | "key": "detectors[].code", 592 | "condition": { "functionBody": "return !model.use_espilight" } 593 | }, 594 | { 595 | "key": "detectors[].pulse", 596 | "condition": { "functionBody": "return !model.use_espilight" } 597 | }, 598 | { 599 | "key": "detectors[].protocol", 600 | "condition": { "functionBody": "return !model.use_espilight" } 601 | }, 602 | { 603 | "key": "detectors[].type", 604 | "condition": { "functionBody": "return model.use_espilight" } 605 | }, 606 | { 607 | "key": "detectors[].message", 608 | "condition": { "functionBody": "return model.use_espilight" }, 609 | "items":[ 610 | { 611 | "type": "flex", 612 | "flex-flow": "row wrap", 613 | "items": [ 614 | "detectors[].message.id", 615 | "detectors[].message.unit", 616 | "detectors[].message.state" 617 | ] 618 | } 619 | ] 620 | } 621 | ] 622 | } 623 | ] 624 | }, 625 | { 626 | "type": "fieldset", 627 | "expandable": true, 628 | "expanded": false, 629 | "title": "Motion Detectors", 630 | "items": [ 631 | { 632 | "key": "motion", 633 | "items":[ 634 | "motion[].name", 635 | "motion[].hint", 636 | { 637 | "key": "motion[].code", 638 | "condition": { "functionBody": "return !model.use_espilight" } 639 | }, 640 | { 641 | "key": "motion[].pulse", 642 | "condition": { "functionBody": "return !model.use_espilight" } 643 | }, 644 | { 645 | "key": "motion[].protocol", 646 | "condition": { "functionBody": "return !model.use_espilight" } 647 | }, 648 | { 649 | "key": "motion[].type", 650 | "condition": { "functionBody": "return model.use_espilight" } 651 | }, 652 | { 653 | "key": "motion[].message", 654 | "condition": { "functionBody": "return model.use_espilight" }, 655 | "items":[ 656 | { 657 | "type": "flex", 658 | "flex-flow": "row wrap", 659 | "items": [ 660 | "motion[].message.id", 661 | "motion[].message.unit", 662 | "motion[].message.state" 663 | ] 664 | } 665 | ] 666 | } 667 | ] 668 | } 669 | ] 670 | } 671 | ] 672 | } 673 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Service, Characteristic; 2 | const SerialTransceiver = require('./tc-serial'); 3 | const WebsocketTransceiver = require('./tc-websocket'); 4 | var sentCodes = []; 5 | 6 | /** PLATFORM CLASS **/ 7 | function ArduinoSwitchPlatform (log, config) { 8 | const self = this; 9 | self.config = config; 10 | self.log = log; 11 | var ioTimeout = 100; 12 | if (config.input_output_timeout) ioTimeout = config.input_output_timeout; 13 | if (config.serial_port || config.serial_port_out) { 14 | const myport = config.serial_port ? config.serial_port : config.serial_port_out; 15 | self.log('Enabling USB mode using ', myport); 16 | self.transceiver = new SerialTransceiver(self.log, myport, ioTimeout); 17 | } else if (config.host) { 18 | const host = config.host; 19 | const port = config.port ? config.port : 80; 20 | self.log('Enabling WebSocket mode using ', host + ':' + port); 21 | self.transceiver = new WebsocketTransceiver(self.log, host, port, ioTimeout); 22 | } else { 23 | self.log('Warning: No USB transceiver configured.'); 24 | } 25 | } 26 | ArduinoSwitchPlatform.prototype.listen = function () { 27 | if(!this.transceiver) return; 28 | this.transceiver.setCallback(this.receiveMessage.bind(this)); 29 | this.transceiver.init(); 30 | }; 31 | ArduinoSwitchPlatform.prototype.accessories = function (callback) { 32 | const self = this; 33 | self.accessories = []; 34 | if (self.config.switches) { 35 | self.config.switches.forEach(function (sw) { 36 | self.accessories.push(new ArduinoSwitchAccessory(sw, self.log, self.config, self.transceiver)); 37 | }); 38 | } 39 | if (self.config.buttons) { 40 | self.config.buttons.forEach(function (sw) { 41 | self.accessories.push(new ArduinoButtonAccessory(sw, self.log, self.config)); 42 | }); 43 | } 44 | if (self.config.detectors) { 45 | self.config.detectors.forEach(function (sw) { 46 | self.accessories.push(new ArduinoSmokeAccessory(sw, self.log, self.config)); 47 | }); 48 | } 49 | if (self.config.sensors) { 50 | self.config.sensors.forEach(function (sw) { 51 | self.accessories.push(new ArduinoWaterAccessory(sw, self.log, self.config)); 52 | }); 53 | } 54 | if (self.config.motion) { 55 | self.config.motion.forEach(function (sw) { 56 | self.accessories.push(new ArduinoMotionAccessory(sw, self.log, self.config)); 57 | }); 58 | } 59 | setTimeout(self.listen.bind(self), 10); 60 | callback(self.accessories); 61 | }; 62 | ArduinoSwitchPlatform.prototype.receiveMessage = function (value) { 63 | const self = this; 64 | var found = false; 65 | if (!checkCode(value, sentCodes, false) && self.accessories) { 66 | self.accessories.forEach(function (accessory) { 67 | if (accessory.notify.call(accessory, value)) found = true; 68 | }); 69 | } else { 70 | found = true; 71 | } 72 | if (!found) { 73 | try { 74 | self.log(JSON.stringify(value)); 75 | } catch (e) { this.log(e); } 76 | } 77 | }; 78 | 79 | module.exports = function (homebridge) { 80 | Service = homebridge.hap.Service; 81 | Characteristic = homebridge.hap.Characteristic; 82 | homebridge.registerPlatform('homebridge-433-arduino', 'ArduinoRCSwitch', ArduinoSwitchPlatform); 83 | }; 84 | 85 | /** SWITCH ACCESSORY CLASS **/ 86 | function ArduinoSwitchAccessory (sw, log, config, transceiver) { 87 | const self = this; 88 | self.name = sw.name; 89 | self.sw = sw; 90 | self.log = log; 91 | self.config = config; 92 | self.transceiver = transceiver; 93 | self.currentState = false; 94 | self.throttle = config.throttle ? config.throttle : 500; 95 | 96 | self.service = new Service.Switch(self.name); 97 | 98 | self.service.getCharacteristic(Characteristic.On).value = self.currentState; 99 | 100 | self.service.getCharacteristic(Characteristic.On).on('get', function (cb) { 101 | cb(null, self.currentState); 102 | }); 103 | 104 | self.service.getCharacteristic(Characteristic.On).on('set', function (state, cb) { 105 | self.currentState = state; 106 | if (self.currentState) { 107 | const out = getSendObject(self.sw, true); 108 | addCode(out, sentCodes); 109 | self.transceiver.send(out); 110 | self.log('Sent on code for %s', self.sw.name); 111 | } else { 112 | const out = getSendObject(self.sw, false); 113 | addCode(out, sentCodes); 114 | self.transceiver.send(out); 115 | self.log('Sent off code for %s', self.sw.name); 116 | } 117 | cb(null); 118 | }); 119 | self.notifyOn = helpers.throttle(function () { 120 | self.log('Received on code for %s', self.sw.name); 121 | self.currentState = true; 122 | self.service.getCharacteristic(Characteristic.On).updateValue(self.currentState); 123 | }, self.throttle, self); 124 | self.notifyOff = helpers.throttle(function () { 125 | self.log('Received off code for %s', self.sw.name); 126 | self.currentState = false; 127 | self.service.getCharacteristic(Characteristic.On).updateValue(self.currentState); 128 | }, self.throttle, self); 129 | }// TODO: code stuff 130 | ArduinoSwitchAccessory.prototype.notify = function (message) { 131 | if (isSameAsSwitch(message, this.sw)) { 132 | if (getSwitchState(message, this.sw)) { 133 | this.notifyOn(); 134 | } else { 135 | this.notifyOff(); 136 | } 137 | return true; 138 | } 139 | return false; 140 | }; 141 | ArduinoSwitchAccessory.prototype.getServices = function () { 142 | const self = this; 143 | var services = []; 144 | var service = new Service.AccessoryInformation(); 145 | service.setCharacteristic(Characteristic.Name, self.name) 146 | .setCharacteristic(Characteristic.Manufacturer, '433 MHz RC') 147 | .setCharacteristic(Characteristic.FirmwareRevision, process.env.version) 148 | .setCharacteristic(Characteristic.HardwareRevision, '1.0.0'); 149 | services.push(service); 150 | services.push(self.service); 151 | return services; 152 | }; 153 | 154 | /** BUTTON ACCESSORY CLASS **/ 155 | function ArduinoButtonAccessory (sw, log, config) { 156 | const self = this; 157 | self.name = sw.name; 158 | self.sw = sw; 159 | self.log = log; 160 | self.config = config; 161 | self.currentState = false; 162 | self.throttle = config.throttle ? config.throttle : 500; 163 | 164 | self.service = new Service.Switch(self.name); 165 | 166 | self.service.getCharacteristic(Characteristic.On).value = self.currentState; 167 | 168 | self.service.getCharacteristic(Characteristic.On).on('get', function (cb) { 169 | cb(null, self.currentState); 170 | }); 171 | 172 | self.service.getCharacteristic(Characteristic.On).on('set', function (state, cb) { 173 | self.currentState = state; 174 | if (state) { 175 | setTimeout(this.resetButton.bind(this), 1000); 176 | } 177 | cb(null); 178 | }.bind(self)); 179 | 180 | self.notifyOn = helpers.throttle(function () { 181 | this.log('Received button code for %s', this.sw.name); 182 | this.currentState = true; 183 | this.service.getCharacteristic(Characteristic.On).updateValue(this.currentState); 184 | setTimeout(this.resetButton.bind(this), 1000); 185 | }, self.throttle, self); 186 | } 187 | ArduinoButtonAccessory.prototype.notify = function (message) { 188 | if (isSameAsSwitch(message, this.sw, true)) { 189 | this.notifyOn(); 190 | return true; 191 | } 192 | return false; 193 | }; 194 | ArduinoButtonAccessory.prototype.resetButton = function () { 195 | this.currentState = false; 196 | this.service.getCharacteristic(Characteristic.On).updateValue(this.currentState); 197 | }; 198 | ArduinoButtonAccessory.prototype.getServices = function () { 199 | const self = this; 200 | var services = []; 201 | var service = new Service.AccessoryInformation(); 202 | service.setCharacteristic(Characteristic.Name, self.name) 203 | .setCharacteristic(Characteristic.Manufacturer, '433 MHz RC') 204 | .setCharacteristic(Characteristic.FirmwareRevision, process.env.version) 205 | .setCharacteristic(Characteristic.HardwareRevision, '1.0.0'); 206 | services.push(service); 207 | services.push(self.service); 208 | return services; 209 | }; 210 | 211 | /** SMOKE ACCESSORY CLASS **/ 212 | function ArduinoSmokeAccessory (sw, log, config) { 213 | const self = this; 214 | self.name = sw.name; 215 | self.sw = sw; 216 | self.log = log; 217 | self.config = config; 218 | self.currentState = false; 219 | self.throttle = config.throttle ? config.throttle : 10000; 220 | self.service = new Service.SmokeSensor(self.name); 221 | self.service.getCharacteristic(Characteristic.SmokeDetected).value = self.currentState; 222 | self.service.getCharacteristic(Characteristic.SmokeDetected).on('get', function (cb) { 223 | cb(null, self.currentState); 224 | }); 225 | self.notifyOn = helpers.throttle(function () { 226 | this.log('Received smoke detector code for %s', this.sw.name); 227 | this.currentState = true; 228 | this.service.getCharacteristic(Characteristic.SmokeDetected).updateValue(this.currentState); 229 | setTimeout(this.resetButton.bind(this), 60000); 230 | }, self.throttle, self); 231 | } 232 | ArduinoSmokeAccessory.prototype.notify = function (message) { 233 | if (isSameAsSwitch(message, this.sw, true)) { 234 | this.notifyOn(); 235 | return true; 236 | } 237 | return false; 238 | }; 239 | ArduinoSmokeAccessory.prototype.resetButton = function () { 240 | this.currentState = false; 241 | this.service.getCharacteristic(Characteristic.SmokeDetected).updateValue(this.currentState); 242 | }; 243 | ArduinoSmokeAccessory.prototype.getServices = function () { 244 | const self = this; 245 | var services = []; 246 | var service = new Service.AccessoryInformation(); 247 | service.setCharacteristic(Characteristic.Name, self.name) 248 | .setCharacteristic(Characteristic.Manufacturer, '433 MHz RC') 249 | .setCharacteristic(Characteristic.FirmwareRevision, process.env.version) 250 | .setCharacteristic(Characteristic.HardwareRevision, '1.0.0'); 251 | services.push(service); 252 | services.push(self.service); 253 | return services; 254 | }; 255 | 256 | /** WATER ACCESSORY CLASS **/ 257 | function ArduinoWaterAccessory (sw, log, config) { 258 | const self = this; 259 | self.name = sw.name; 260 | self.sw = sw; 261 | self.log = log; 262 | self.config = config; 263 | self.currentState = false; 264 | self.throttle = config.throttle ? config.throttle : 10000; 265 | self.service = new Service.LeakSensor(self.name); 266 | self.service.getCharacteristic(Characteristic.LeakDetected).value = self.currentState; 267 | self.service.getCharacteristic(Characteristic.LeakDetected).on('get', function (cb) { 268 | cb(null, self.currentState); 269 | }); 270 | self.notifyOn = helpers.throttle(function () { 271 | this.log('Received leak detector code for %s', this.sw.name); 272 | this.currentState = true; 273 | this.service.getCharacteristic(Characteristic.LeakDetected).updateValue(this.currentState); 274 | setTimeout(this.resetButton.bind(this), 60000); 275 | }, self.throttle, self); 276 | } 277 | ArduinoWaterAccessory.prototype.notify = function (message) { 278 | if (isSameAsSwitch(message, this.sw, true)) { 279 | this.notifyOn(); 280 | return true; 281 | } 282 | return false; 283 | }; 284 | ArduinoWaterAccessory.prototype.resetButton = function () { 285 | this.currentState = false; 286 | this.service.getCharacteristic(Characteristic.LeakDetected).updateValue(this.currentState); 287 | }; 288 | ArduinoWaterAccessory.prototype.getServices = function () { 289 | const self = this; 290 | var services = []; 291 | var service = new Service.AccessoryInformation(); 292 | service.setCharacteristic(Characteristic.Name, self.name) 293 | .setCharacteristic(Characteristic.Manufacturer, '433 MHz RC') 294 | .setCharacteristic(Characteristic.FirmwareRevision, process.env.version) 295 | .setCharacteristic(Characteristic.HardwareRevision, '1.0.0'); 296 | services.push(service); 297 | services.push(self.service); 298 | return services; 299 | }; 300 | 301 | /** MOTION DETECTOR ACCESSORY CLASS **/ 302 | function ArduinoMotionAccessory (sw, log, config) { 303 | const self = this; 304 | self.name = sw.name; 305 | self.sw = sw; 306 | self.log = log; 307 | self.config = config; 308 | self.currentState = false; 309 | self.throttle = config.throttle ? config.throttle : 10000; 310 | self.service = new Service.MotionSensor(self.name); 311 | self.service.getCharacteristic(Characteristic.MotionDetected).value = self.currentState; 312 | self.service.getCharacteristic(Characteristic.MotionDetected).on('get', function (cb) { 313 | cb(null, self.currentState); 314 | }); 315 | self.notifyOn = helpers.throttle(function () { 316 | this.log('Received motion detector code for %s', this.sw.name); 317 | this.currentState = true; 318 | this.service.getCharacteristic(Characteristic.MotionDetected).updateValue(this.currentState); 319 | setTimeout(this.resetButton.bind(this), 10000); 320 | }, self.throttle, self); 321 | } 322 | ArduinoMotionAccessory.prototype.notify = function (message) { 323 | if (isSameAsSwitch(message, this.sw, true)) { 324 | this.notifyOn(); 325 | return true; 326 | } 327 | return false; 328 | }; 329 | ArduinoMotionAccessory.prototype.resetButton = function () { 330 | this.currentState = false; 331 | this.service.getCharacteristic(Characteristic.MotionDetected).updateValue(this.currentState); 332 | }; 333 | ArduinoMotionAccessory.prototype.getServices = function () { 334 | const self = this; 335 | var services = []; 336 | var service = new Service.AccessoryInformation(); 337 | service.setCharacteristic(Characteristic.Name, self.name) 338 | .setCharacteristic(Characteristic.Manufacturer, '433 MHz RC') 339 | .setCharacteristic(Characteristic.FirmwareRevision, process.env.version) 340 | .setCharacteristic(Characteristic.HardwareRevision, '1.0.0'); 341 | services.push(service); 342 | services.push(self.service); 343 | return services; 344 | }; 345 | 346 | /** HELPERS SECTION **/ 347 | var helpers = { 348 | throttle: function (fn, threshold, scope) { 349 | threshold || (threshold = 250); 350 | var last; 351 | 352 | return function () { 353 | var context = scope || this; 354 | var now = +new Date(); var args = arguments; 355 | 356 | if (last && now < last + threshold) { 357 | } else { 358 | last = now; 359 | fn.apply(context, args); 360 | } 361 | }; 362 | } 363 | }; 364 | 365 | function checkCode (value, array, remove) { 366 | var index = array.findIndex(imSameMessage, value); 367 | if (index > -1) { 368 | if (remove) array.splice(index, 1); 369 | return true; 370 | } else { 371 | return false; 372 | } 373 | } 374 | 375 | function addCode (value, array) { 376 | array.push(value); 377 | setTimeout(checkCode, 2000, value, array, true); 378 | } 379 | 380 | function getSwitchState (message, sw) { 381 | if (message.code && sw.on) { 382 | if (message.code === sw.on.code) return true; 383 | } else if (message.code && sw.off) { 384 | if (message.code === sw.off.code) return false; 385 | } else if (message.code && sw.code) { 386 | if (message.code === sw.code) return true; 387 | } else if (message.message && message.message.state) { 388 | const state = message.message.state; 389 | if (state === 'on') return true; 390 | if (state === 'off') return false; 391 | if (state === 'up') return true; 392 | if (state === 'down') return false; 393 | if (state === 1) return true; 394 | if (state === 0) return false; 395 | if (state === '1') return true; 396 | if (state === '0') return false; 397 | } 398 | return false; 399 | } 400 | // TODO not needed since isSameMessage on/off addition? 401 | function isSameAsSwitch (message, sw, compareState = false) { 402 | if (sw.on && sw.off) { // on/off format 403 | if (isSameMessage(message, sw.on, compareState)) return true; 404 | if (isSameMessage(message, sw.off, compareState)) return true; 405 | } else { // button/espilight format 406 | if (isSameMessage(message, sw, compareState)) return true; 407 | } 408 | return false; 409 | } 410 | function imSameMessage (message) { 411 | // int idx = sentCodes.findIndex(imSameMessage, sw); 412 | // "this" is compare message info or switch info 413 | return isSameMessage(message, this, true); 414 | } 415 | function isSameMessage (message, prototype, compareState = false) { 416 | if (!message || !prototype) return; 417 | if (message.code && prototype.code) { 418 | if (prototype.code == message.code) return true; 419 | } else if (message.code && prototype.on) { 420 | if (prototype.on.code == message.code) return true; 421 | } else if (message.code && prototype.off) { 422 | if (prototype.off.code == message.code) return true; 423 | } 424 | // TODO: other kinds of espilight messages without id/unit 425 | else if (message.type && prototype.type) { 426 | if (prototype.type == message.type && 427 | prototype.message.id == message.message.id && 428 | prototype.message.unit == message.message.unit) { 429 | if (compareState) { 430 | if (prototype.message.state == message.message.state) { 431 | return true; 432 | } 433 | } else return true; 434 | } 435 | } 436 | return false; 437 | } 438 | // make a new object to send 439 | function getSendObject (sw, on = undefined) { 440 | var out = {}; 441 | if (sw.on && on === true) { 442 | out.code = sw.on.code; 443 | out.pulse = sw.on.pulse; 444 | out.protocol = sw.on.protocol ? sw.on.protocol : 1; 445 | } else if (sw.off && on === false) { 446 | out.code = sw.off.code; 447 | out.pulse = sw.off.pulse; 448 | out.protocol = sw.off.protocol ? sw.off.protocol : 1; 449 | } else if (sw.code) { 450 | out.code = sw.code; 451 | out.pulse = sw.pulse; 452 | out.protocol = sw.protocol ? sw.protocol : 1; 453 | } else if (sw.type && sw.message) { // different for on/off switches 454 | out.type = sw.type; 455 | out.message = makeTransmitMessage(sw.message, on); 456 | } 457 | return out; 458 | } 459 | // change message from "state":"off" to "off":1 etc. 460 | // if on is undefined use state, else change to value of on 461 | function makeTransmitMessage (message, on = undefined) { 462 | if (!message) return message; 463 | try { 464 | var clonedMessage = JSON.parse(JSON.stringify(message)); 465 | } catch (e) { this.log(e); } 466 | if (!clonedMessage) return {}; 467 | if (clonedMessage.state) { 468 | const state = clonedMessage.state; 469 | if (state === 'on') { 470 | if (on === undefined) { 471 | clonedMessage.on = 1; 472 | } else if (on) { 473 | clonedMessage.on = 1; 474 | } else { 475 | clonedMessage.off = 1; 476 | } 477 | } else if (state === 'up') { 478 | if (on === undefined) { 479 | clonedMessage.up = 1; 480 | } else if (on) { 481 | clonedMessage.up = 1; 482 | } else { 483 | clonedMessage.down = 1; 484 | } 485 | } else if (state === 'off') { 486 | if (on === undefined) { 487 | clonedMessage.off = 1; 488 | } else if (on) { 489 | clonedMessage.on = 1; 490 | } else { 491 | clonedMessage.off = 1; 492 | } 493 | } else if (state === 'down') { 494 | if (on === undefined) { 495 | clonedMessage.down = 1; 496 | } else if (on) { 497 | clonedMessage.up = 1; 498 | } else { 499 | clonedMessage.down = 1; 500 | } 501 | } else if (on) { 502 | clonedMessage.on = 1; 503 | } else if (on !== undefined) { 504 | clonedMessage.off = 1; 505 | } 506 | delete clonedMessage.state; 507 | } else if (on) { 508 | clonedMessage.on = 1; 509 | } else if (on !== undefined) { 510 | clonedMessage.off = 1; 511 | } 512 | // make id and unit numbers if possible 513 | if (clonedMessage.id) { 514 | const conv = Number(clonedMessage.id); 515 | if (!Number.isNaN(conv)) { 516 | clonedMessage.id = conv; 517 | } 518 | } 519 | if (clonedMessage.unit) { 520 | const conv = Number(clonedMessage.unit); 521 | if (!Number.isNaN(conv)) { 522 | clonedMessage.unit = conv; 523 | } 524 | } 525 | return clonedMessage; 526 | } 527 | -------------------------------------------------------------------------------- /normensTemplates.template: -------------------------------------------------------------------------------- 1 | name:433_Clarus_fix 2 | filter:TYPE=IT 3 | desc:Fix a 433 "Clarus" device 4 | order:A0 5 | par:NEWDEF;new definition;{my $str = InternalVal("DEVICE", "DEF", "xx");;$str =~ s/0F F0/01 10/g;;$str} 6 | attr DEVICE ITclock 188 7 | attr DEVICE room IT_CLARUS 8 | modify DEVICE NEWDEF 9 | 10 | name:433_Intertechno_fix 11 | filter:TYPE=IT 12 | desc:Fix a 433 "Intertechno Old" device 13 | order:A1 14 | attr DEVICE ITclock 320 15 | attr DEVICE room IT_FIXED 16 | 17 | name:Add_Siri_Name 18 | desc:Make Siri name from device name (Room.Device -> Device Room) 19 | order:09 20 | par:SIRINAME;new name;{my $str = InternalVal("DEVICE", "NAME", "xx");;my @words = split(/\./, $str);;my $output = join(" ", reverse @words);;$output} 21 | attr DEVICE siriName SIRINAME 22 | 23 | name:Make_MotionSensor 24 | filter:TYPE=IT 25 | desc:Configure a 433 device as MotionSensor, set NAME beforehand! 26 | order:11 27 | par:ICON;ICON as set, defaults to motion_detector;{ AttrVal('DEVICE','icon','motion_detector') } 28 | attr DEVICE icon ICON 29 | attr DEVICE devStateIcon on:people_sensor off:message_presence 30 | # remove on/off 31 | attr DEVICE webCmd : 32 | attr DEVICE genericDeviceType MotionSensor 33 | attr DEVICE homebridgeMapping MotionDetected=state,values=on:1;;off:0 34 | attr DEVICE room IT_MOTION 35 | defmod DEVICE.wd watchdog DEVICE:on 00:00:05 DEVICE:off setreading DEVICE state off 36 | attr DEVICE.wd autoRestart 1 37 | attr DEVICE.wd room IT_MOTION 38 | 39 | name:Make_LeakSensor 40 | filter:TYPE=IT 41 | desc:Configure a 433 device as LeakSensor, set NAME beforehand! 42 | order:12 43 | par:ICON;ICON as set, defaults to humidity;{ AttrVal('DEVICE','icon','humidity') } 44 | attr DEVICE icon ICON 45 | attr DEVICE devStateIcon on:humidity off:general_ok 46 | # remove on/off 47 | attr DEVICE webCmd : 48 | attr DEVICE genericDeviceType LeakSensor 49 | attr DEVICE homebridgeMapping LeakDetected=state,values=on:1;;off:0 50 | attr DEVICE room IT_LEAK 51 | defmod DEVICE.wd watchdog DEVICE:on 00:01 DEVICE:off setreading DEVICE state off 52 | attr DEVICE.wd autoRestart 1 53 | attr DEVICE.wd room IT_LEAK 54 | 55 | name:Make_SmokeDetect 56 | filter:TYPE=IT 57 | desc:Configure a 433 device as Smoke Detector, set NAME beforehand! 58 | order:13 59 | par:ICON;ICON as set, defaults to secur_smoke_detector;{ AttrVal('DEVICE','icon','secur_smoke_detector') } 60 | attr DEVICE icon ICON 61 | attr DEVICE devStateIcon off:general_ok .*:secur_alarm 62 | # remove on/off 63 | attr DEVICE webCmd : 64 | attr DEVICE genericDeviceType SmokeSensor 65 | attr DEVICE homebridgeMapping SmokeDetected=state,values=on:SMOKE_DETECTED;;off:SMOKE_NOT_DETECTED 66 | attr DEVICE room IT_SMOKE 67 | defmod DEVICE.wd watchdog DEVICE:on 00:01 DEVICE:off setreading DEVICE state off 68 | attr DEVICE.wd autoRestart 1 69 | attr DEVICE.wd room IT_SMOKE 70 | 71 | name:Make_Button 72 | filter:TYPE=IT 73 | desc:Configure a 433 device as Single Press Button (auto reset), set NAME beforehand! 74 | order:14 75 | attr DEVICE devStateIcon on:ring .*:control_home 76 | # remove on/off 77 | attr DEVICE webCmd : 78 | attr DEVICE genericDeviceType StatelessProgrammableSwitch 79 | attr DEVICE homebridgeMapping ProgrammableSwitchEvent=state,values=on:SINGLE_PRESS,nocache=1,timeout=1 80 | attr DEVICE room IT_BUTTON 81 | defmod DEVICE.wd watchdog DEVICE:on 00:00:01 DEVICE:off setreading DEVICE state off 82 | attr DEVICE.wd autoRestart 1 83 | attr DEVICE.wd room IT_BUTTON 84 | 85 | name:Add_Switches 86 | #filter:TYPE=IT 87 | desc: Adds StatelessProgrammableSwitches for on/off, set NAME and siriName beforehand! 88 | order:20 89 | par:SIRINAME;new name;{my $str = InternalVal("DEVICE", "NAME", "xx");;my @words = split(/\./, $str);;my $generated = join(" ", reverse @words);;my $output = AttrVal("DEVICE", "siriName", $generated);;$generated} 90 | #par:SIRINAME;siri name;{AttrVal("DEVICE", "siriName", "SIRIGEN" )} 91 | defmod DEVICE.on dummy 92 | attr DEVICE.on genericDeviceType StatelessProgrammableSwitch 93 | attr DEVICE.on homebridgeMapping ProgrammableSwitchEvent=state,values=on:SINGLE_PRESS;;orf:SINGLE_PRESS,nocache=1,timeout=1 94 | attr DEVICE.on siriName SIRINAME On 95 | attr DEVICE.on room IT_SWITCH 96 | defmod DEVICE.on.notify notify DEVICE {if($EVENT eq "on"){ fhem "set DEVICE.on on" }} 97 | attr DEVICE.on.notify room IT_SWITCH 98 | defmod DEVICE.on.wd watchdog DEVICE.on:on 00:00:01 DEVICE.on:off setreading DEVICE.on state off 99 | attr DEVICE.on.wd room IT_SWITCH 100 | attr DEVICE.on.wd autoRestart 1 101 | defmod DEVICE.off dummy 102 | attr DEVICE.off genericDeviceType StatelessProgrammableSwitch 103 | attr DEVICE.off homebridgeMapping ProgrammableSwitchEvent=state,values=on:SINGLE_PRESS;;orf:SINGLE_PRESS,nocache=1,timeout=1 104 | attr DEVICE.off siriName SIRINAME Off 105 | attr DEVICE.off room IT_SWITCH 106 | defmod DEVICE.off.notify notify DEVICE {if($EVENT eq "off"){ fhem "set DEVICE.off on" }} 107 | attr DEVICE.off.notify room IT_SWITCH 108 | defmod DEVICE.off.wd watchdog DEVICE.off:on 00:00:01 DEVICE.off:off setreading DEVICE.off state off 109 | attr DEVICE.off.wd room IT_SWITCH 110 | attr DEVICE.off.wd autoRestart 1 111 | attr DEVICE room IT_SWITCH 112 | 113 | name:Remove_Switches 114 | #filter:TYPE=IT 115 | desc: Removes StatelessProgrammableSwitches for on/off 116 | order:21 117 | delete DEVICE.on 118 | delete DEVICE.on.notify 119 | delete DEVICE.on.wd 120 | delete DEVICE.off 121 | delete DEVICE.off.notify 122 | delete DEVICE.off.wd 123 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-433-arduino", 3 | "version": "0.12.3", 4 | "description": "Add bidirectional support for 433MHz switches to Homebridge using Arduino or ESP hardware.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/homebridge/bin/homebridge -P ./ -I -U ./.homebridge" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/normen/homebridge-433-arduino.git" 12 | }, 13 | "keywords": [ 14 | "homebridge-plugin", 15 | "433", 16 | "rcswitch" 17 | ], 18 | "author": "Normen Hansen", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/normen/homebridge-433-arduino/issues" 22 | }, 23 | "homepage": "https://github.com/normen/homebridge-433-arduino#readme", 24 | "engines": { 25 | "homebridge": ">=0.4.0", 26 | "node": ">=0.12.0" 27 | }, 28 | "dependencies": { 29 | "bufferutil": "^4.0.1", 30 | "serialport": "^9.0.2", 31 | "utf-8-validate": "^5.0.2", 32 | "ws": "^7.2.1" 33 | }, 34 | "devDependencies": { 35 | "homebridge": "^1.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tc-serial.js: -------------------------------------------------------------------------------- 1 | const SerialPort = require('serialport'); 2 | const Readline = SerialPort.parsers.Readline; 3 | 4 | function SerialTransceiver (log, portName, ioTimeout = 500) { 5 | this.log = log; 6 | this.portName = portName; 7 | this.ioTimeout = ioTimeout; 8 | this.blockPort = null; 9 | this.inPort = null; 10 | } 11 | 12 | SerialTransceiver.prototype.setCallback = function (callback) { 13 | this.callback = callback; 14 | }; 15 | 16 | SerialTransceiver.prototype.init = function () { 17 | this.port = new SerialPort(this.portName, { baudRate: 9600 }); 18 | this.inPort = this.port.pipe(new Readline({ delimiter: '\n' })); 19 | this.blockPort = new Device(this.port, this.ioTimeout); 20 | this.inPort.on('data', this.serialCallback.bind(this)); 21 | }; 22 | 23 | SerialTransceiver.prototype.send = function (message) { 24 | if (message.code) { 25 | this.blockPort.send(message.code + '/' + message.pulse + '/' + message.protocol + '\n'); 26 | } else if (message.type && message.message) { 27 | try { 28 | this.blockPort.send(JSON.stringify(message) + '\n'); 29 | } catch (e) { this.log(e); } 30 | } 31 | }; 32 | 33 | SerialTransceiver.prototype.serialCallback = function (data) { 34 | if (data.startsWith('OK')) return; 35 | this.blockPort.lastInputTime = new Date().getTime(); 36 | if (data.startsWith('{')) { 37 | try { 38 | const message = JSON.parse(data); 39 | this.callback(message); 40 | } catch (e) { this.log(e); } 41 | return; 42 | } 43 | if (data.startsWith('pilight')) { 44 | this.log(data); 45 | return; 46 | } 47 | var content = data.split('/'); 48 | if (content.length >= 2) { 49 | var value = content[0]; 50 | var pulse = content[1].replace('\n', '').replace('\r', ''); 51 | var protocol = content[2]; 52 | if (protocol) { 53 | protocol = protocol.replace('\n', '').replace('\r', ''); 54 | } else { 55 | protocol = 1; 56 | } 57 | this.callback({ code: Number(value), pulse: Number(pulse), protocol: Number(protocol) }); 58 | } 59 | }; 60 | 61 | /* private class device, send to serial port with queue */ 62 | function Device (serial, ioTimeout = 500) { 63 | this.lastInputTime = 0; 64 | this.ioTimeout = ioTimeout; 65 | this._serial = serial; 66 | this._queue = []; 67 | this._busy = false; 68 | var device = this; 69 | var pipedSerial = serial.pipe(new Readline({ delimiter: '\n' })); 70 | pipedSerial.on('data', function (data) { 71 | if (data.startsWith('OK')) { 72 | device.processQueue(); 73 | } 74 | }); 75 | } 76 | Device.prototype.send = function (data) { 77 | this._queue.push(data); 78 | if (this._busy) return; 79 | this._busy = true; 80 | this.processQueue(); 81 | }; 82 | Device.prototype.processQueue = function (inData) { 83 | var next = inData === undefined ? this._queue.shift() : inData; 84 | if (!next) { 85 | this._busy = false; 86 | return; 87 | } 88 | var curTime = new Date().getTime(); 89 | if (curTime - this.lastInputTime < this.ioTimeout) { 90 | setTimeout(this.processQueue.bind(this, next), this.ioTimeout); 91 | } else { 92 | this._serial.write(next); 93 | } 94 | }; 95 | 96 | module.exports = SerialTransceiver; 97 | -------------------------------------------------------------------------------- /tc-websocket.js: -------------------------------------------------------------------------------- 1 | const WebSocketClient = require('./websocket-client'); 2 | 3 | function WebsocketTransceiver (log, host, port = 80, ioTimeout = 500) { 4 | this.log = log; 5 | this.host = host; 6 | this.port = port; 7 | 8 | this._queue = []; 9 | this._busy = false; 10 | 11 | this.lastInputTime = 0; 12 | this.ioTimeout = ioTimeout; 13 | 14 | this.ws = new WebSocketClient(log); 15 | } 16 | 17 | WebsocketTransceiver.prototype.setCallback = function (callback) { 18 | this.callback = callback; 19 | }; 20 | 21 | WebsocketTransceiver.prototype.init = function () { 22 | this.ws.open('ws://' + this.host + ':' + this.port); 23 | this.ws.onmessage = this.wsCallback.bind(this); 24 | }; 25 | 26 | WebsocketTransceiver.prototype.send = function (message) { 27 | var msg = null; 28 | if (message.code) { 29 | msg = message.code + '/' + message.pulse + '/' + message.protocol; 30 | } else if (message.type && message.message) { 31 | try { 32 | msg = JSON.stringify(message); 33 | } catch (e) { this.log(e); } 34 | } 35 | if (msg == null) return; 36 | this._queue.push(msg); 37 | if (this._busy) return; 38 | this._busy = true; 39 | this.processQueue(); 40 | }; 41 | 42 | WebsocketTransceiver.prototype.processQueue = function (inData) { 43 | var next = inData === undefined ? this._queue.shift() : inData; 44 | if (!next) { 45 | this._busy = false; 46 | return; 47 | } 48 | var curTime = new Date().getTime(); 49 | if (curTime - this.lastInputTime < this.ioTimeout) { 50 | setTimeout(this.processQueue.bind(this, next), this.ioTimeout); 51 | } else { 52 | this.ws.send(next); 53 | } 54 | }; 55 | 56 | WebsocketTransceiver.prototype.wsCallback = function (data) { 57 | if (data.startsWith('OK')) { 58 | this.processQueue(); 59 | return; 60 | } 61 | this.lastInputTime = new Date().getTime(); 62 | if (data.startsWith('{')) { 63 | try { 64 | const message = JSON.parse(data); 65 | this.callback(message); 66 | } catch (e) { this.log(e); } 67 | return; 68 | } 69 | if (data.startsWith('pilight')) { 70 | this.log(data); 71 | return; 72 | } 73 | var content = data.split('/'); 74 | if (content.length >= 2) { 75 | var value = content[0]; 76 | var pulse = content[1].replace('\n', '').replace('\r', ''); 77 | var protocol = content[2]; 78 | if (protocol) { 79 | protocol = protocol.replace('\n', '').replace('\r', ''); 80 | } else { 81 | protocol = 1; 82 | } 83 | this.callback({ code: Number(value), pulse: Number(pulse), protocol: Number(protocol) }); 84 | } 85 | }; 86 | 87 | module.exports = WebsocketTransceiver; 88 | -------------------------------------------------------------------------------- /websocket-client.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | 3 | function WebSocketClient (log) { 4 | this.log = log; 5 | this.number = 0; // Message number 6 | this.autoReconnectInterval = 20 * 1000; // ms 7 | this.pongReturned = true; 8 | } 9 | WebSocketClient.prototype.open = function (url) { 10 | const self = this; 11 | this.url = url; 12 | this.instance = new WebSocket(this.url); 13 | this.instance.on('open', () => { 14 | this.onopen(); 15 | self.pongReturned = true; 16 | self.checkPing(); 17 | }); 18 | this.instance.on('message', (data, flags) => { 19 | this.number++; 20 | this.onmessage(data, flags, this.number); 21 | }); 22 | this.instance.on('close', (e) => { 23 | switch (e.code) { 24 | case 1000: // CLOSE_NORMAL 25 | this.reconnect(e); 26 | break; 27 | default: // Abnormal closure 28 | this.reconnect(e); 29 | break; 30 | } 31 | this.onclose(e); 32 | }); 33 | this.instance.on('error', (e) => { 34 | switch (e.code) { 35 | case 'ECONNREFUSED': 36 | this.reconnect(e); 37 | break; 38 | default: 39 | this.reconnect(e); 40 | break; 41 | } 42 | this.onerror(e); 43 | }); 44 | this.instance.on('pong', (e) => { 45 | self.pongReturned = true; 46 | }); 47 | }; 48 | WebSocketClient.prototype.checkPing = function () { 49 | if (!this.pongReturned) { 50 | this.instance.terminate(); 51 | } else { 52 | this.pongReturned = false; 53 | this.instance.ping('ping'); 54 | clearTimeout(this.pingTimeout); 55 | this.pingTimeout = setTimeout(this.checkPing.bind(this), this.autoReconnectInterval); 56 | } 57 | }; 58 | WebSocketClient.prototype.send = function (data, option) { 59 | try { 60 | this.instance.send(data, option); 61 | } catch (e) { 62 | this.instance.emit('error', e); 63 | } 64 | }; 65 | WebSocketClient.prototype.reconnect = function (e) { 66 | const self = this; 67 | this.instance.removeAllListeners(); 68 | var that = this; 69 | setTimeout(function () { 70 | self.log('WebSocketClient: reconnecting...'); 71 | that.open(that.url); 72 | }, this.autoReconnectInterval); 73 | }; 74 | WebSocketClient.prototype.onopen = function (e) { this.log('WebSocketClient: open', arguments); }; 75 | WebSocketClient.prototype.onmessage = function (data, flags, number) { this.log('WebSocketClient: message', arguments); }; 76 | WebSocketClient.prototype.onerror = function (e) { this.log('WebSocketClient: error', arguments); }; 77 | WebSocketClient.prototype.onclose = function (e) { this.log('WebSocketClient: closed', arguments); }; 78 | 79 | module.exports = WebSocketClient; 80 | --------------------------------------------------------------------------------