├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── nodejs.yml ├── .gitignore ├── test ├── homebridge │ └── config.json ├── init.js └── testserver.js ├── package.json ├── LICENSE ├── index.js └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Supereg 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .npmignore 4 | .jshintrc 5 | .idea 6 | **/*.iml 7 | .DS_Store -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Additional context** 14 | Add any other context about the feature request here. 15 | -------------------------------------------------------------------------------- /test/homebridge/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge", 4 | "username": "AE:72:E6:5D:1D:83", 5 | "port": 51826, 6 | "pin": "897-23-872" 7 | }, 8 | 9 | "accessories": [ 10 | { 11 | "accessory": "HTTP-SWITCH", 12 | "name": "Test Switch", 13 | 14 | "onUrl": { 15 | "url": "http://localhost:8085/on" 16 | }, 17 | "offUrl": "http://localhost:8085/off", 18 | 19 | "statusUrl": { 20 | "url": "http://localhost:8085/get", 21 | "method": "GET" 22 | }, 23 | 24 | "pullInterval": 5000, 25 | 26 | "debug": true 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Expected behavior** 11 | A clear and concise description of what you expected to happen. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. ... 16 | 2. ... 17 | 3. ... 18 | 19 | **Version** (output of `npm list -g homebridge homebridge-http-switch`) 20 | - homebridge: 21 | - homebridge-http-switch: 22 | 23 | **Configuration** 24 | ```json 25 | Your configuration goes in here 26 | ``` 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-http-switch", 3 | "version": "0.5.36", 4 | "description": "Powerful http switch for Homebridge", 5 | "license": "ISC", 6 | "keywords": [ 7 | "homebridge-plugin" 8 | ], 9 | "scripts": { 10 | "test": "echo no test defined" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Supereg/homebridge-http-switch" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/Supereg/homebridge-http-switch/issues" 18 | }, 19 | "engines": { 20 | "node": ">=10.17.0", 21 | "homebridge": ">=0.4.8" 22 | }, 23 | "dependencies": { 24 | "homebridge-http-base": "~2.1.13" 25 | }, 26 | "devDependencies": { 27 | "homebridge": "^1.5.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License (ISC) 2 | Copyright (c) 2017-2019, Andreas Bauer 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any 5 | purpose with or without fee is hereby granted, provided that the above 6 | copyright notice and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node-CI 2 | 3 | on: [push, pull_request, create] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 12.x, 13.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install 21 | run: npm ci 22 | env: 23 | CI: true 24 | - name: npm build 25 | run: npm run build --if-present 26 | env: 27 | CI: true 28 | - name: npm test 29 | run: npm test 30 | env: 31 | CI: true 32 | 33 | publish-npm: 34 | if: github.repository == 'Supereg/homebridge-http-switch' && github.event_name == 'create' && startsWith(github.ref, 'refs/tags/v') 35 | 36 | needs: build 37 | 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - uses: actions/checkout@v1 42 | - uses: actions/setup-node@v1 43 | with: 44 | node-version: 10 45 | registry-url: https://registry.npmjs.org/ 46 | - run: npm ci 47 | - run: npm publish 48 | env: 49 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 50 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | const spawn = require('child_process').spawn; 2 | const path = require('path'); 3 | const fs = require("fs"); 4 | const TestServer = require('./testserver'); 5 | 6 | 7 | 8 | const server = new TestServer(); 9 | server.start(); 10 | 11 | const pluginPath = path.join(__dirname, "../"); 12 | const storagePath = path.join(__dirname, 'homebridge'); 13 | 14 | const args = `--plugin-path ${pluginPath} --user-storage-path ${storagePath} --debug`; // TODO debug option?; --no-qrcode 15 | 16 | const homebridge = spawn('homebridge', args.split(' '), {env: process.env}); // TODO maybe test with hap-nodejs, this method needs homebridge installed globally 17 | homebridge.stderr.on('data', data => { 18 | console.log(String(data)); 19 | }); 20 | homebridge.stdout.on('data', data => { 21 | console.log(String(data)); 22 | }); 23 | 24 | const signals = { 'SIGINT': 2, 'SIGTERM': 15 }; 25 | Object.keys(signals).forEach(signal => { 26 | process.on(signal, () => { 27 | console.log("Got %s, shutting down tests...", signal); 28 | 29 | console.log("Cleaning up files..."); 30 | 31 | deleteDirectorySync(path.join(storagePath, "accessories")); 32 | deleteDirectorySync(path.join(storagePath, "persist")); 33 | 34 | homebridge.kill("SIGKILL"); 35 | 36 | setTimeout(() => { 37 | process.exit(0); 38 | }); 39 | }); 40 | }); 41 | 42 | function deleteDirectorySync(pathname) { 43 | 44 | if (fs.existsSync(pathname)) { 45 | let files = fs.readdirSync(pathname); 46 | 47 | files.forEach(file => { 48 | const nextPath = path.join(pathname, file); 49 | 50 | if (fs.lstatSync(nextPath).isDirectory()) 51 | deleteDirectorySync(pathname); 52 | else 53 | fs.unlinkSync(nextPath); 54 | }); 55 | 56 | fs.rmdirSync(pathname); 57 | } 58 | } -------------------------------------------------------------------------------- /test/testserver.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | 4 | let value = false; 5 | 6 | module.exports = TestServer; 7 | 8 | function TestServer() { 9 | // TODO maybe make this configurable. However ist currently hardcoded in config.json 10 | this.port = 8085; 11 | } 12 | 13 | TestServer.prototype.start = function () { 14 | http.createServer(this.handleHTTPCall.bind(this)).listen(this.port); 15 | console.log("Listening on 8085..."); 16 | }; 17 | 18 | TestServer.prototype.handleHTTPCall = function (request, response) { 19 | if (request.method !== "GET") { 20 | response.writeHead(405, {'Content-Type': "text/html"}); 21 | response.write("Method Not Allowed"); 22 | response.end(); 23 | 24 | console.log("Someone tried to access the server without an GET request"); 25 | return; 26 | } 27 | 28 | const parts = url.parse(request.url, true); 29 | 30 | const pathname = parts.pathname.charAt(0) === "/" 31 | ? parts.pathname.substring(1) 32 | : parts.pathname; 33 | const path = pathname.split("/"); 34 | 35 | if (path.length === 0) { 36 | response.writeHead(400, {'Content-Type': "text/html"}); 37 | response.write("Bad Request"); 38 | response.end(); 39 | 40 | console.log("Bad Request: " + parts.pathname); 41 | return; 42 | } 43 | 44 | switch (path[0]) { 45 | case "get": 46 | response.writeHead(200, {'Content-Type': "text/html"}); 47 | response.write(value? "1": "0"); 48 | response.end(); 49 | 50 | value = !value; 51 | break; 52 | case "on": 53 | value = true; 54 | response.writeHead(200, {'Content-Type': "text/html"}); 55 | response.end(); 56 | break; 57 | case "off": 58 | value = false; 59 | response.writeHead(200, {'Content-Type': "text/html"}); 60 | response.end(); 61 | break; 62 | default: 63 | response.writeHead(404, {'Content-Type': "text/html"}); 64 | response.write("Not Found"); 65 | response.end(); 66 | 67 | console.log("Route not found: " + path[0]); 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | let Service, Characteristic, api; 4 | 5 | const _http_base = require("homebridge-http-base"); 6 | const http = _http_base.http; 7 | const configParser = _http_base.configParser; 8 | const PullTimer = _http_base.PullTimer; 9 | const notifications = _http_base.notifications; 10 | const MQTTClient = _http_base.MQTTClient; 11 | const Cache = _http_base.Cache; 12 | const utils = _http_base.utils; 13 | 14 | const packageJSON = require('./package.json'); 15 | 16 | module.exports = function (homebridge) { 17 | Service = homebridge.hap.Service; 18 | Characteristic = homebridge.hap.Characteristic; 19 | 20 | api = homebridge; 21 | 22 | homebridge.registerAccessory("homebridge-http-switch", "HTTP-SWITCH", HTTP_SWITCH); 23 | }; 24 | 25 | const SwitchType = Object.freeze({ 26 | STATEFUL: "stateful", 27 | STATELESS: "stateless", 28 | STATELESS_REVERSE: "stateless-reverse", 29 | TOGGLE: "toggle", 30 | TOGGLE_REVERSE: "toggle-reverse", 31 | }); 32 | 33 | function HTTP_SWITCH(log, config) { 34 | this.log = log; 35 | this.name = config.name; 36 | this.debug = config.debug || false; 37 | 38 | this.switchType = utils.enumValueOf(SwitchType, config.switchType, SwitchType.STATEFUL); 39 | if (!this.switchType) { 40 | this.log.warn(`'${this.switchType}' is a invalid switchType! Aborting...`); 41 | return; 42 | } 43 | 44 | this.timeout = config.timeout; 45 | if (typeof this.timeout !== 'number') { 46 | this.timeout = 1000; 47 | } 48 | 49 | if (config.serialNumber !== undefined && typeof config.serialNumber === "string") { 50 | this.serialNumber = config.serialNumber; 51 | } 52 | 53 | if (this.switchType === SwitchType.STATEFUL) { 54 | this.statusPattern = /1/; 55 | if (config.statusPattern) { 56 | if (typeof config.statusPattern === "string") 57 | this.statusPattern = new RegExp(config.statusPattern); 58 | else 59 | this.log.warn("Property 'statusPattern' was given in an unsupported type. Using default one!"); 60 | } 61 | 62 | this.statusCache = new Cache(config.statusCache, 0); 63 | if (config.statusCache && typeof config.statusCache !== "number") 64 | this.log.warn("Property 'statusCache' was given in an unsupported type. Using default one!"); 65 | } 66 | 67 | /** @namespace config.multipleUrlExecutionStrategy */ 68 | if (config.multipleUrlExecutionStrategy) { 69 | const result = http.setMultipleUrlExecutionStrategy(config.multipleUrlExecutionStrategy); 70 | 71 | if (!result) 72 | this.log.warn("'multipleUrlExecutionStrategy' has an invalid value (" + config.multipleUrlExecutionStrategy + "). Continuing with defaults!"); 73 | } 74 | 75 | const success = this.parseUrls(config); // parsing 'onUrl', 'offUrl', 'statusUrl' 76 | if (!success) { 77 | this.log.warn("Aborting..."); 78 | return; 79 | } 80 | 81 | /** @namespace config.httpMethod */ 82 | if (config.httpMethod) { // if we have it defined globally override the existing one of ON and OFF config object 83 | this.log("Global 'httpMethod' is specified. Overriding method of on and off!"); 84 | if (this.on) 85 | this.on.forEach(urlObject => urlObject.method = config.httpMethod); 86 | if (this.off) 87 | this.off.forEach(urlObject => urlObject.method = config.httpMethod); 88 | 89 | /* 90 | * New way would expect to also override method of this.status, but old implementation used fixed 'httpMethod' (GET) 91 | * for this.status and was unaffected by this config property. So we leave this.status unaffected for now to maintain 92 | * backwards compatibility. 93 | */ 94 | } 95 | 96 | if (config.auth) { 97 | if (!(config.auth.username && config.auth.password)) 98 | this.log("'auth.username' and/or 'auth.password' was not set!"); 99 | else { 100 | if (this.on) { 101 | this.on.forEach(urlObject => { 102 | urlObject.auth.username = config.auth.username; 103 | urlObject.auth.password = config.auth.password; 104 | 105 | if (typeof config.auth.sendImmediately === "boolean") 106 | urlObject.auth.sendImmediately = config.auth.sendImmediately; 107 | }); 108 | } 109 | if (this.off) { 110 | this.off.forEach(urlObject => { 111 | urlObject.auth.username = config.auth.username; 112 | urlObject.auth.password = config.auth.password; 113 | 114 | if (typeof config.auth.sendImmediately === "boolean") 115 | urlObject.auth.sendImmediately = config.auth.sendImmediately; 116 | }); 117 | } 118 | if (this.status) { 119 | this.status.auth.username = config.auth.username; 120 | this.status.auth.password = config.auth.password; 121 | 122 | if (typeof config.auth.sendImmediately === "boolean") 123 | this.status.auth.sendImmediately = config.auth.sendImmediately; 124 | } 125 | } 126 | } 127 | 128 | this.homebridgeService = new Service.Switch(this.name); 129 | const onCharacteristic = this.homebridgeService.getCharacteristic(Characteristic.On) 130 | .on("get", this.getStatus.bind(this)) 131 | .on("set", this.setStatus.bind(this)); 132 | 133 | 134 | switch (this.switchType) { 135 | case SwitchType.TOGGLE_REVERSE: 136 | case SwitchType.STATELESS_REVERSE: 137 | onCharacteristic.updateValue(true); 138 | break; 139 | } 140 | 141 | /** @namespace config.pullInterval */ 142 | if (config.pullInterval) { 143 | if (this.switchType === SwitchType.STATEFUL) { 144 | this.pullTimer = new PullTimer(this.log, config.pullInterval, this.getStatus.bind(this), value => { 145 | this.homebridgeService.getCharacteristic(Characteristic.On).updateValue(value); 146 | }); 147 | this.pullTimer.start(); 148 | } 149 | else 150 | this.log("'pullInterval' was specified, however switch is stateless. Ignoring property and not enabling pull updates!"); 151 | } 152 | 153 | if (config.notificationID) { 154 | if (this.switchType === SwitchType.STATEFUL 155 | || this.switchType === SwitchType.TOGGLE || this.switchType === SwitchType.TOGGLE_REVERSE) { 156 | /** @namespace config.notificationPassword */ 157 | /** @namespace config.notificationID */ 158 | notifications.enqueueNotificationRegistrationIfDefined(api, log, config.notificationID, config.notificationPassword, this.handleNotification.bind(this)); 159 | } 160 | else 161 | this.log("'notificationID' was specified, however switch is stateless. Ignoring property and not enabling notifications!"); 162 | } 163 | 164 | if (config.mqtt) { 165 | if (this.switchType === SwitchType.STATEFUL 166 | || this.switchType === SwitchType.TOGGLE || this.switchType === SwitchType.TOGGLE_REVERSE) { 167 | let options; 168 | try { 169 | options = configParser.parseMQTTOptions(config.mqtt) 170 | } catch (error) { 171 | this.log.error("Error occurred while parsing MQTT property: " + error.message); 172 | this.log.error("MQTT will not be enabled!"); 173 | } 174 | 175 | if (options) { 176 | try { 177 | this.mqttClient = new MQTTClient(this.homebridgeService, options, this.log); 178 | this.mqttClient.connect(); 179 | } catch (error) { 180 | this.log.error("Error occurred creating mqtt client: " + error.message); 181 | } 182 | } 183 | } 184 | else 185 | this.log("'mqtt' options were specified, however switch is stateless. Ignoring it!"); 186 | } 187 | 188 | this.log("Switch successfully configured..."); 189 | if (this.debug) { 190 | this.log("Switch started with the following options: "); 191 | this.log(" - switchType: " + this.switchType); 192 | if (this.switchType === SwitchType.STATEFUL) 193 | this.log(" - statusPattern: " + this.statusPattern); 194 | 195 | if (this.auth) 196 | this.log(" - auth options: " + JSON.stringify(this.auth)); 197 | 198 | if (this.on) 199 | this.log(" - onUrls: " + JSON.stringify(this.on)); 200 | if (this.off) 201 | this.log(" - offUrls: " + JSON.stringify(this.off)); 202 | if (this.status) 203 | this.log(" - statusUrl: " + JSON.stringify(this.status)); 204 | 205 | if (this.switchType === SwitchType.STATELESS || this.switchType === SwitchType.STATELESS_REVERSE) 206 | this.log(" - timeout for stateless switch: " + this.timeout); 207 | 208 | if (this.pullTimer) 209 | this.log(" - pullTimer started with interval " + config.pullInterval); 210 | 211 | if (config.notificationID) 212 | this.log(" - notificationsID specified: " + config.notificationID); 213 | 214 | if (this.mqttClient) { 215 | const options = this.mqttClient.mqttOptions; 216 | this.log(` - mqtt client instantiated: ${options.protocol}://${options.host}:${options.port}`); 217 | this.log(" -> subscribing to topics:"); 218 | 219 | for (const topic in this.mqttClient.subscriptions) { 220 | if (!this.mqttClient.subscriptions.hasOwnProperty(topic)) 221 | continue; 222 | 223 | this.log(` - ${topic}`); 224 | } 225 | } 226 | } 227 | } 228 | 229 | HTTP_SWITCH.prototype = { 230 | 231 | parseUrls: function (config) { 232 | /** @namespace config.onUrl */ 233 | if (this.switchType !== SwitchType.STATELESS_REVERSE) { 234 | if (config.onUrl) { 235 | try { 236 | this.on = this.switchType === SwitchType.STATEFUL 237 | ? [configParser.parseUrlProperty(config.onUrl)] 238 | : configParser.parseMultipleUrlProperty(config.onUrl); 239 | } catch (error) { 240 | this.log.warn("Error occurred while parsing 'onUrl': " + error.message); 241 | return false; 242 | } 243 | } 244 | else { 245 | this.log.warn(`Property 'onUrl' is required when using switchType '${this.switchType}'`); 246 | return false; 247 | } 248 | } 249 | else if (config.onUrl) 250 | this.log.warn(`Property 'onUrl' is defined though it is not used with switchType ${this.switchType}. Ignoring it!`); 251 | 252 | /** @namespace config.offUrl */ 253 | if (this.switchType !== SwitchType.STATELESS) { 254 | if (config.offUrl) { 255 | try { 256 | this.off = this.switchType === SwitchType.STATEFUL 257 | ? [configParser.parseUrlProperty(config.offUrl)] 258 | : configParser.parseMultipleUrlProperty(config.offUrl); 259 | } catch (error) { 260 | this.log.warn("Error occurred while parsing 'offUrl': " + error.message); 261 | return false; 262 | } 263 | } 264 | else { 265 | this.log.warn(`Property 'offUrl' is required when using switchType '${this.switchType}'`); 266 | return false; 267 | } 268 | } 269 | else if (config.offUrl) 270 | this.log.warn(`Property 'offUrl' is defined though it is not used with switchType ${this.switchType}. Ignoring it!`); 271 | 272 | if (this.switchType === SwitchType.STATEFUL) { 273 | /** @namespace config.statusUrl */ 274 | if (config.statusUrl) { 275 | try { 276 | this.status = configParser.parseUrlProperty(config.statusUrl); 277 | } catch (error) { 278 | this.log.warn("Error occurred while parsing 'statusUrl': " + error.message); 279 | return false; 280 | } 281 | } 282 | else { 283 | this.log.warn(`Property 'statusUrl' is required when using switchType '${this.switchType}'`); 284 | return false; 285 | } 286 | } 287 | else if (config.statusUrl) 288 | this.log.warn(`Property 'statusUrl' is defined though it is not used with switchType ${this.switchType}. Ignoring it!`); 289 | 290 | return true; 291 | }, 292 | 293 | identify: function (callback) { 294 | this.log("Identify requested!"); 295 | callback(); 296 | }, 297 | 298 | getServices: function () { 299 | if (!this.homebridgeService) 300 | return []; 301 | 302 | const informationService = new Service.AccessoryInformation(); 303 | 304 | informationService 305 | .setCharacteristic(Characteristic.Manufacturer, "Andreas Bauer") 306 | .setCharacteristic(Characteristic.Model, "HTTP Switch") 307 | .setCharacteristic(Characteristic.SerialNumber, this.serialNumber || "SW01") 308 | .setCharacteristic(Characteristic.FirmwareRevision, packageJSON.version); 309 | 310 | return [informationService, this.homebridgeService]; 311 | }, 312 | 313 | /** @namespace body.characteristic */ 314 | handleNotification: function(body) { 315 | const value = body.value; 316 | 317 | let characteristic; 318 | switch (body.characteristic) { 319 | case "On": 320 | characteristic = Characteristic.On; 321 | break; 322 | default: 323 | this.log("Encountered unknown characteristic handling notification: " + body.characteristic); 324 | return; 325 | } 326 | 327 | if (this.debug) 328 | this.log("Updating '" + body.characteristic + "' to new value: " + body.value); 329 | 330 | if (this.pullTimer) 331 | this.pullTimer.resetTimer(); 332 | 333 | this.homebridgeService.getCharacteristic(characteristic).updateValue(value); 334 | }, 335 | 336 | getStatus: function (callback) { 337 | if (this.pullTimer) 338 | this.pullTimer.resetTimer(); 339 | 340 | switch (this.switchType) { 341 | case SwitchType.STATEFUL: 342 | if (!this.statusCache.shouldQuery()) { 343 | const value = this.homebridgeService.getCharacteristic(Characteristic.On).value; 344 | if (this.debug) 345 | this.log(`getStatus() returning cached value '${value? "ON": "OFF"}'${this.statusCache.isInfinite()? " (infinite cache)": ""}`); 346 | callback(null, value); 347 | break; 348 | } 349 | 350 | if (this.debug) 351 | this.log("getStatus() doing http request..."); 352 | 353 | http.httpRequest(this.status, (error, response, body) => { 354 | if (error) { 355 | this.log("getStatus() failed: %s", error.message); 356 | callback(error); 357 | } 358 | else if (!(http.isHttpSuccessCode(response.statusCode) || http.isHttpRedirectCode(response.statusCode))) { 359 | this.log("getStatus() http request returned http error code: %s", response.statusCode); 360 | callback(new Error("Got html error code " + response.statusCode)); 361 | } 362 | else { 363 | if (this.debug) 364 | this.log(`getStatus() request returned successfully (${response.statusCode}). Body: '${body}'`); 365 | 366 | if (http.isHttpRedirectCode(response.statusCode)) { 367 | this.log("getStatus() http request return with redirect status code (3xx). Accepting it anyways"); 368 | } 369 | 370 | const switchedOn = this.statusPattern.test(body); 371 | if (this.debug) 372 | this.log("Switch is currently %s", switchedOn? "ON": "OFF"); 373 | 374 | this.statusCache.queried(); // we only update lastQueried on successful query 375 | callback(null, switchedOn); 376 | } 377 | }); 378 | break; 379 | case SwitchType.STATELESS: 380 | callback(null, false); 381 | break; 382 | case SwitchType.STATELESS_REVERSE: 383 | callback(null, true); 384 | break; 385 | case SwitchType.TOGGLE: 386 | case SwitchType.TOGGLE_REVERSE: 387 | callback(null, this.homebridgeService.getCharacteristic(Characteristic.On).value); 388 | break; 389 | 390 | default: 391 | callback(new Error("Unrecognized switch type")); 392 | break; 393 | } 394 | }, 395 | 396 | setStatus: function (on, callback) { 397 | if (this.pullTimer) 398 | this.pullTimer.resetTimer(); 399 | 400 | switch (this.switchType) { 401 | case SwitchType.STATEFUL: 402 | this._makeSetRequest(on, callback); 403 | break; 404 | case SwitchType.STATELESS: 405 | if (!on) { 406 | callback(); 407 | break; 408 | } 409 | 410 | this._makeSetRequest(true, callback); 411 | break; 412 | case SwitchType.STATELESS_REVERSE: 413 | if (on) { 414 | callback(); 415 | break; 416 | } 417 | 418 | this._makeSetRequest(false, callback); 419 | break; 420 | case SwitchType.TOGGLE: 421 | case SwitchType.TOGGLE_REVERSE: 422 | this._makeSetRequest(on, callback); 423 | break; 424 | 425 | default: 426 | callback(new Error("Unrecognized switch type")); 427 | break; 428 | } 429 | }, 430 | 431 | _makeSetRequest: function (on, callback) { 432 | const urlObjectArray = on? this.on: this.off; 433 | 434 | if (this.debug) 435 | this.log("setStatus() doing http request..."); 436 | 437 | http.multipleHttpRequests(urlObjectArray, results => { 438 | const errors = []; 439 | const successes = []; 440 | 441 | results.forEach((result, i) => { 442 | if (result.error) { 443 | errors.push({ 444 | index: i, 445 | error: result.error 446 | }); 447 | } 448 | else if (!(http.isHttpSuccessCode(result.response.statusCode) || http.isHttpRedirectCode(result.response.statusCode))) { 449 | errors.push({ 450 | index: i, 451 | error: new Error(`HTTP request returned with error code ${result.response.statusCode}`), 452 | value: result.body 453 | }); 454 | } 455 | else { 456 | if (http.isHttpRedirectCode(result.response.statusCode)) { 457 | this.log("setStatus() http request return with redirect status code (3xx). Accepting it anyways"); 458 | } 459 | 460 | successes.push({ 461 | index: i, 462 | value: result.body 463 | }); 464 | } 465 | }); 466 | 467 | if (errors.length > 0) { 468 | if (successes.length === 0) { 469 | if (errors.length === 1) { 470 | const errorObject = errors[0]; 471 | const errorMessage = errorObject.error.message; 472 | this.log(`Error occurred setting state of switch: ${errorMessage}`); 473 | 474 | if (errorMessage && !errorMessage.startsWith("HTTP request returned with error code ")) 475 | this.log(errorObject.error); 476 | else if (errorObject.value && this.debug) 477 | this.log("Body of set response is: " + errorObject.value); 478 | } 479 | else { 480 | this.log(`Error occurred setting state of switch with every request (${errors.length}):`); 481 | this.log(errors); 482 | } 483 | } 484 | else { 485 | this.log(`${successes.length} requests successfully set switch to ${on? "ON": "OFF"}; ${errors.length} encountered and error:`); 486 | this.log(errors); 487 | } 488 | 489 | callback(new Error("Some or every request returned with an error. See above!")); 490 | } 491 | else { 492 | if (this.debug) 493 | this.log(`Successfully set switch to ${on ? "ON" : "OFF"}${successes.length > 1 ? ` with every request (${successes.length})` : ""}`); 494 | callback(); 495 | } 496 | 497 | this.resetSwitchWithTimeoutIfStateless(); 498 | }); 499 | }, 500 | 501 | resetSwitchWithTimeoutIfStateless: function () { 502 | switch (this.switchType) { 503 | case SwitchType.STATELESS: 504 | this.log("Resetting switch to OFF"); 505 | 506 | setTimeout(() => { 507 | this.homebridgeService.setCharacteristic(Characteristic.On, false); 508 | }, this.timeout); 509 | break; 510 | case SwitchType.STATELESS_REVERSE: 511 | this.log("Resetting switch to ON"); 512 | 513 | setTimeout(() => { 514 | this.homebridgeService.setCharacteristic(Characteristic.On, true); 515 | }, this.timeout); 516 | break; 517 | } 518 | }, 519 | 520 | }; 521 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-http-switch Plugin 2 | 3 | [![npm](https://img.shields.io/npm/v/homebridge-http-switch?style=for-the-badge)](https://www.npmjs.com/package/homebridge-http-switch) 4 | [![npm](https://img.shields.io/npm/dt/homebridge-http-switch?style=for-the-badge)](https://www.npmjs.com/package/homebridge-http-switch) 5 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/Supereg/homebridge-http-switch/Node-CI?style=for-the-badge)](https://github.com/Supereg/homebridge-http-switch/actions?query=workflow%3A%22Node-CI%22) 6 | [![GitHub issues](https://img.shields.io/github/issues/Supereg/homebridge-http-switch?style=for-the-badge)](https://github.com/Supereg/homebridge-http-switch/issues) 7 | [![GitHub pull requests](https://img.shields.io/github/issues-pr/Supereg/homebridge-http-switch?style=for-the-badge)](https://github.com/Supereg/homebridge-http-switch/pulls) 8 | 9 | 10 | `homebridge-http-switch` is a [Homebridge](https://github.com/nfarina/homebridge) plugin with which you can configure 11 | HomeKit switches which forward any requests to a defined http server. This comes in handy when you already have home 12 | automated equipment which can be controlled via http requests. Or you have built your own equipment, for example some sort 13 | of lightning controlled with an wifi enabled Arduino board which than can be integrated via this plugin into Homebridge. 14 | 15 | `homebridge-http-switch` supports three different type of switches. A normal `stateful` switch and two variants of 16 | _stateless_ switches (`stateless` and `stateless-reverse`) which differ in their original position. For stateless switches 17 | you can specify multiple urls to be targeted when the switch is turned On/Off. 18 | More about on how to configure such switches can be read further down. 19 | 20 | ## Installation 21 | 22 | First of all you need to have [Homebridge](https://github.com/nfarina/homebridge) installed. Refer to the repo for 23 | instructions. 24 | Then run the following command to install `homebridge-http-switch` 25 | 26 | ``` 27 | sudo npm install -g homebridge-http-switch 28 | ``` 29 | 30 | ## Updating the switch state in HomeKit 31 | 32 | The _'On'_ characteristic from the _'switch'_ service has the permission to `notify` the HomeKit controller of state 33 | changes. `homebridge-http-switch` supports two ways to send state changes to HomeKit. 34 | 35 | ### The 'pull' way: 36 | 37 | The 'pull' way is probably the easiest to set up and supported in every scenario. `homebridge-http-switch` requests the 38 | state of the switch in an specified interval (pulling) and sends the value to HomeKit. 39 | Look for `pullInterval` in the list of configuration options if you want to configure it. 40 | 41 | ### The 'push' way: 42 | 43 | When using the 'push' concept, the http device itself sends the updated value to `homebridge-http-switch` whenever 44 | the value changes. This is more efficient as the new value is updated instantly and `homebridge-http-switch` does not 45 | need to make needless requests when the value didn't actually change. 46 | However because the http device needs to actively notify the `homebridge-http-switch` there is more work needed 47 | to implement this method into your http device. 48 | 49 | #### Using MQTT: 50 | 51 | MQTT (Message Queuing Telemetry Transport) is a protocol widely used by IoT devices. IoT devices can publish messages 52 | on a certain topic to the MQTT broker which then sends this message to all clients subscribed to the specified topic. 53 | In order to use MQTT you need to setup a broker server ([mosquitto](https://github.com/eclipse/mosquitto) is a solid 54 | open source MQTT broker running perfectly on a device like the Raspberry Pi) and then instruct all clients to 55 | publish/subscribe to it. 56 | For [shelly.cloud](https://shelly.cloud) devices mqtt is the best and only option to implement push-updates. 57 | 58 | #### Using 'homebridge-http-notification-server': 59 | 60 | For those of you who are developing the http device by themselves I developed a pretty simple 'protocol' based on http 61 | to send push-updates. 62 | How to implement the protocol into your http device can be read in the chapter 63 | [**Notification Server**](#notification-server) 64 | 65 | ## Configuration: 66 | 67 | The configuration can contain the following properties: 68 | 69 | #### Basic configuration options: 70 | 71 | - `name` \ **required**: Defines the name which is later displayed in HomeKit 72 | - `switchType` \ **optional** \(Default: **"stateful"**\): Defines the type of the switch: 73 | * **"stateful"**: A normal switch and thus the default value. 74 | * **"stateless"**: A stateless switch remains in only one state. If you switch it to on, it immediately goes back to off. 75 | Configuration example is further [down](#stateless-switch). 76 | * **"stateless-reverse"**: Default position is ON. If you switch it to off, it immediately goes back to on. 77 | Configuration example is further [down](#reverse-stateless-switch). 78 | * **"toggle"**: The toggle switch is a stateful switch however does not use the `statusUrl` to determine the current 79 | state. It uses the last set state as the current state. Default position is OFF. 80 | * **"toggle-reverse""**: Same as **"toggle"** but switch default position is ON. 81 | 82 | * `onUrl` \ **required**: Defines the url 83 | (and other properties when using an urlObject) which is called when you turn on the switch. 84 | * `offUrl` \ **required**: Defines the url 85 | (and other properties when using an urlObject) which is called when you turn off the switch. 86 | * `statusUrl` \ **required**: Defines the url 87 | (and other properties when using an urlObject) to query the current state from the switch. By default it expects the http 88 | server to return **'1'** for ON and **'0'** for OFF leaving out any html markup. 89 | You can change this using `statusPattern` option. 90 | 91 | #### Advanced configuration options: 92 | 93 | - `serialNumber` \ **optional** \(Default: **"SW01"**\): Defines a custom serial number shown in the home app. 94 | - `statusPattern` \ **optional** \(Default: **"1"**\): Defines a regex pattern which is compared to the body of the `statusUrl`. 95 | When matching the status of the switch is set to ON otherwise OFF. [Some examples](#examples-for-custom-statuspatterns). 96 | - `statusCache` \ **optional** \(Default: **0**\): Defines the amount of time in milliseconds a queried state 97 | of the switch is cached before a new request is made to the http device. 98 | Default is **0** which indicates no caching. A value of **-1** will indicate infinite caching. 99 | - `auth` \ **optional**: If your http server requires authentication you can specify your credential in this 100 | object. It uses those credentials for all http requests and thus overrides all possibly specified credentials inside 101 | an urlObject for `onUrl`, `offUrl` and `statusUrl`. 102 | The object can contain the following properties: 103 | * `username` \ **required** 104 | * `password` \ **required** 105 | * `sendImmediately` \ **optional** \(Default: **true**\): When set to **true** the plugin will send the 106 | credentials immediately to the http server. This is best practice for basic authentication. 107 | When set to **false** the plugin will send the proper authentication header after receiving an 401 error code 108 | (unauthenticated). The response must include a proper `WWW-Authenticate` header. 109 | Digest authentication requires this property to be set to **false**! 110 | - `httpMethod` _**deprecated**_ \ **optional**: If defined it sets the http method for `onUrl` and `offUrl`. 111 | This property is deprecated and only present for backwards compatibility. It is recommended to use an 112 | [[urlObject](#urlobject)] to set the http method per url. 113 | 114 | * `timeout` \ **optional** \(Default: **1000**\): When using a stateless switch this timeout in 115 | **milliseconds** specifies the time after which the switch is reset back to its original state. 116 | * `pullInterval` \ **optional**: The property expects an interval in **milliseconds** in which the plugin 117 | pulls updates from your http device. For more information read [pulling updates](#the-pull-way). 118 | (This option is only supported when `switchType` is **"stateful"**) 119 | 120 | - `mqtt` \<[mqttObject](#mqttobject)\> **optional**: Defines all properties used for mqtt connection. 121 | See [mqttObject](#mqttobject). 122 | 123 | * `multipleUrlExecutionStrategy` \ **optional** \(Default: **"parallel"**\): Defines the strategy used when 124 | executing multiple urls. The following are available: 125 | * **"parallel"**: All urls are executed in parallel. No particular order is guaranteed. Execution as fast as possible. 126 | * **"series"**: All urls are executed in the given order. Each url must complete first before the next one is executed. 127 | When using series execution you can also have a look at the [delay url](#the-delay-url). 128 | 129 | - `debug` \ **optional**: If set to true debug mode is enabled and the plugin prints more detailed information. 130 | 131 | Below are two example configurations. One is using simple string urls and the other is using simple urlObjects. 132 | Both configs can be used for a basic plugin configuration. 133 | 134 | ```json 135 | { 136 | "accessories": [ 137 | { 138 | "accessory": "HTTP-SWITCH", 139 | "name": "Switch", 140 | 141 | "switchType": "stateful", 142 | 143 | "onUrl": "http://localhost/api/switchOn", 144 | "offUrl": "http://localhost/api/switchOff", 145 | 146 | "statusUrl": "http://localhost/api/switchStatus" 147 | } 148 | ] 149 | } 150 | ``` 151 | 152 | ```json 153 | { 154 | "accessories": [ 155 | { 156 | "accessory": "HTTP-SWITCH", 157 | "name": "Switch", 158 | 159 | "switchType": "stateful", 160 | 161 | "onUrl": { 162 | "url": "http://localhost/api/switchOn", 163 | "method": "GET" 164 | }, 165 | "offUrl": { 166 | "url": "http://localhost/api/switchOff", 167 | "method": "GET" 168 | }, 169 | 170 | "statusUrl": { 171 | "url": "http://localhost/api/switchStatus", 172 | "method": "GET" 173 | } 174 | } 175 | ] 176 | } 177 | ``` 178 | 179 | #### UrlObject 180 | 181 | A urlObject can have the following properties: 182 | * `url` \ **required**: Defines the url pointing to your http server 183 | * `method` \ **optional** \(Default: **"GET"**\): Defines the http method used to make the http request 184 | * `body` \ **optional**: Defines the body sent with the http request. If value is not a string it will be 185 | converted to a JSON string automatically. 186 | * `strictSSL` \ **optional** \(Default: **false**\): If enabled the SSL certificate used must be valid and 187 | the whole certificate chain must be trusted. The default is false because most people will work with self signed 188 | certificates in their homes and their devices are already authorized since being in their networks. 189 | * `auth` \ **optional**: If your http server requires authentication you can specify your credential in this 190 | object. When defined the object can contain the following properties: 191 | * `username` \ **required** 192 | * `password` \ **required** 193 | * `sendImmediately` \ **optional** \(Default: **true**\): When set to **true** the plugin will send the 194 | credentials immediately to the http server. This is best practice for basic authentication. 195 | When set to **false** the plugin will send the proper authentication header after receiving an 401 error code 196 | (unauthenticated). The response must include a proper `WWW-Authenticate` header. 197 | Digest authentication requires this property to be set to **false**! 198 | * `headers` \ **optional**: Using this object you can define any http headers which are sent with the http 199 | request. The object must contain only string key value pairs. 200 | * `requestTimeout` \ **optional** \(Default: **20000**\): Time in milliseconds specifying timeout (Time to wait 201 | for http response and also setting socket timeout). 202 | * `repeat` \ **optional** \(Default: **1**\): Defines how often the execution of this urlObject should 203 | be repeated. 204 | Notice that this property only has an effect on ulrObject specified in `onUrl` or `offUrl`. 205 | Also have a look at the `multipleUrlExecutionStrategy` property. Using "parallel" execution could result in 206 | unpredictable behaviour. 207 | * `delayBeforeExecution` \ **optional** \(Default: **0**\): Defines the time in milliseconds to wait 208 | before executing the urlObject. 209 | Notice that this property only has an effect on ulrObject specified in `onUrl` or `offUrl`. 210 | Also have a look at the `multipleUrlExecutionStrategy` property. 211 | 212 | Below is an example of an urlObject containing the basic properties: 213 | ```json 214 | { 215 | "url": "http://example.com:8080", 216 | "method": "GET", 217 | "body": "exampleBody", 218 | 219 | "strictSSL": false, 220 | 221 | "auth": { 222 | "username": "yourUsername", 223 | "password": "yourPassword" 224 | }, 225 | 226 | "headers": { 227 | "Content-Type": "text/html" 228 | } 229 | } 230 | ``` 231 | 232 | #### MQTTObject 233 | 234 | A mqttObject can have the following properties: 235 | 236 | ##### Basic configuration options: 237 | 238 | * `host` \ **required**: Defines the host of the mqtt broker. 239 | * `port` \ **optional** \(Default: **1883**\): Defines the port of the mqtt broker. 240 | * `credentials` \ **optional**: Defines the credentials used to authenticate with the mqtt broker. 241 | * `username` \ **required** 242 | * `password` \ **optional** 243 | - `subscriptions` \ **required**: Defines an array (or one single object) of subscriptions. 244 | - `topic` \ **required**: Defines the topic to subscribe to. 245 | - `characteristic` \ **required**: Defines the characteristic this subscription updates. 246 | - `messagePattern` \ **optional**: Defines a regex pattern. If `messagePattern` is not specified the 247 | message received will be used as value. If the characteristic expects a boolean value it is tested if the 248 | specified regex is contained in the received message. Otherwise the pattern is matched against the message 249 | and the data from regex group can be extracted using the given `patternGroupToExtract`. 250 | - `patternGroupToExtract` \ **optional** \(Default: **1**\): Defines the regex group of which data is 251 | extracted. 252 | 253 | ##### Advanced configuration options: 254 | 255 | * `protocol` \ **optional** \(Default: **"mqtt"**\): Defines protocol used to connect to the mqtt broker 256 | * `qos` \ **optional** \(Default: **1**\): Defines the Quality of Service (Notice, the QoS of the publisher 257 | must also be configured accordingly). 258 | In contrast to most implementations the default value is **1**. 259 | * `0`: 'At most once' - the message is sent only once and the client and broker take no additional steps to 260 | acknowledge delivery (fire and forget). 261 | * `1`: 'At least once' - the message is re-tried by the sender multiple times until acknowledgement is 262 | received (acknowledged delivery). 263 | * `2`: 'Exactly once' - the sender and receiver engage in a two-level handshake to ensure only one copy of the 264 | message is received (assured delivery). 265 | * `clientId` \ **optional** \(Default: `'mqttjs_' + Math.random().toString(16).substr(2, 8)`\): Defines clientId 266 | * `keepalive` \ **optional** \(Default: **60**\): Time in seconds to send a keepalive. Set to 0 to disable. 267 | * `clean` \ **optional** \(Default: **true**\): Set to false to receive QoS 1 and 2 messages while offline. 268 | * `reconnectPeriod` \ **optional** \(Default: **1000**\): Time in milliseconds after which a reconnect is tried. 269 | * `connectTimeout` \ **optional** \(Default: **30000**\): Time in milliseconds the client waits until the 270 | CONNECT needs to be acknowledged (CONNACK). 271 | 272 | Below is an example of an mqttObject containing the basic properties for a switch service: 273 | ```json 274 | { 275 | "host": "127.0.0.1", 276 | "port": 1883, 277 | 278 | "credentials": { 279 | "username": "yourUsername", 280 | "password": "yourPassword" 281 | }, 282 | 283 | "subscriptions": [ 284 | { 285 | "topic": "your/topic/here", 286 | "characteristic": "On", 287 | "messagePattern": "on" 288 | } 289 | ] 290 | } 291 | ``` 292 | 293 | ### Stateless Switch 294 | 295 | Since **OFF** is the only possible state you do not need to declare `offUrl` and `statusUrl` 296 | 297 | ```json 298 | { 299 | "accessories": [ 300 | { 301 | "accessory": "HTTP-SWITCH", 302 | "name": "Switch", 303 | 304 | "switchType": "stateless", 305 | 306 | "timeout": 1000, 307 | 308 | "onUrl": "http://localhost/api/switchOn" 309 | } 310 | ] 311 | } 312 | ``` 313 | 314 | ### Reverse Stateless Switch 315 | 316 | Since **ON** is the only possible state you do not need to declare `onUrl` and `statusUrl` 317 | 318 | ```json 319 | { 320 | "accessories": [ 321 | { 322 | "accessory": "HTTP-SWITCH", 323 | "name": "Switch", 324 | 325 | "switchType": "stateless-reverse", 326 | 327 | "timeout": 1000, 328 | 329 | "offUrl": "http://localhost/api/switchOff" 330 | } 331 | ] 332 | } 333 | ``` 334 | 335 | ### Multiple On or Off Urls 336 | If you wish to do so you can specify an array of urls or urlObjects (`onUrl` or `offUrl`) when your switch is a 337 | **stateless switch** or a **reverse-stateless switch**. 338 | **This is not possible with a normal stateful switch.** 339 | 340 | Below are two example configurations of an stateless switch with three urls. 341 | One is using simple string array and the other is using simple urlObject arrays. 342 | 343 | ```json 344 | { 345 | "accessories": [ 346 | { 347 | "accessory": "HTTP-SWITCH", 348 | "name": "Switch", 349 | 350 | "switchType": "stateless", 351 | "onUrl": [ 352 | "http://localhost/api/switch1On", 353 | "http://localhost/api/switch2On", 354 | "http://localhost/api/switch3On" 355 | ] 356 | } 357 | ] 358 | } 359 | ``` 360 | ```json 361 | { 362 | "accessories": [ 363 | { 364 | "accessory": "HTTP-SWITCH", 365 | "name": "Switch", 366 | 367 | "switchType": "stateless", 368 | "onUrl": [ 369 | { 370 | "url": "http://localhost/api/switch1On" 371 | }, 372 | { 373 | "url": "http://localhost/api/switch2On" 374 | }, 375 | { 376 | "url": "http://localhost/api/switch3On" 377 | } 378 | ] 379 | } 380 | ] 381 | } 382 | ``` 383 | 384 | #### The 'delay(...)' url 385 | 386 | When using multiple urls and **"series"** as `multipleUrlExecutionStrategy` you can also specify so called delay urls in the 387 | `onUrl` or `offUrl` arrays. This could be used to guarantee a certain delay between two urls. 388 | The delay url has the following pattern: **"delay(INTEGER)"** where 'INTEGER' is replaced with the delay in milliseconds. 389 | 390 | Here is an example: 391 | ```json 392 | { 393 | "accessories": [ 394 | { 395 | "accessory": "HTTP-SWITCH", 396 | "name": "Delayed Switch", 397 | 398 | "switchType": "stateless", 399 | "multipleUrlExecutionStrategy": "series", 400 | 401 | "onUrl": [ 402 | "http://localhost/api/switch1On", 403 | "delay(1000)", 404 | "http://localhost/api/switch2On" 405 | ] 406 | } 407 | ] 408 | } 409 | ``` 410 | 411 | ### Examples for custom statusPatterns 412 | 413 | The `statusPattern` property can be used to change the phrase which is used to identify if the switch should be turned on 414 | or off. So when you want the switch to be turned on when your server sends **"true"** in the body of the http response you 415 | could specify the following pattern: 416 | ```json 417 | { 418 | "statusPattern": "true" 419 | } 420 | ``` 421 | 422 | However using Regular Expressions much more complex patterns are possible. Let's assume your http enabled device responds 423 | with the following json string as body, where one property has an random value an the other indicates the status of the 424 | switch: 425 | ```json 426 | { 427 | "perRequestRandomValue": 89723789, 428 | "switchState": true 429 | } 430 | ``` 431 | Then you could use the following pattern: 432 | ```json 433 | { 434 | "statusPattern": "{\n \"perRequestRandomValue\": [0-9]+,\n \"switchState\": true\n}" 435 | } 436 | ``` 437 | **Note:** The `statusPattern` must be placed on the same level as the `statusUrl` property, not inside the `statusUrl` object. See below for example. 438 | 439 | ```json 440 | { 441 | "statusUrl": { 442 | }, 443 | "statusPattern": "....", 444 | } 445 | ``` 446 | 447 | More on how to build regex patterns: https://www.w3schools.com/jsref/jsref_obj_regexp.asp 448 | 449 | ## Notification Server 450 | 451 | `homebridge-http-switch` can be used together with 452 | [homebridge-http-notification-server](https://github.com/Supereg/homebridge-http-notification-server) in order to receive 453 | updates when the state changes at your external program. For details on how to implement those updates and how to 454 | install and configure `homebridge-http-notification-server`, please refer to the 455 | [README](https://github.com/Supereg/homebridge-http-notification-server) of the repository first. 456 | 457 | Down here is an example on how to configure `homebridge-http-switch` to work with your implementation of the 458 | `homebridge-http-notification-server`. 459 | 460 | ```json 461 | { 462 | "accessories": [ 463 | { 464 | "accessory": "HTTP-SWITCH", 465 | "name": "Switch", 466 | 467 | "notificationID": "my-switch", 468 | "notificationPassword": "superSecretPassword", 469 | 470 | "onUrl": "http://localhost/api/switchOn", 471 | "offUrl": "http://localhost/api/switchOff", 472 | 473 | "statusUrl": "http://localhost/api/switchStatus" 474 | } 475 | ] 476 | } 477 | ``` 478 | 479 | * `notificationID` is an per Homebridge instance unique id which must be included in any http request. 480 | * `notificationPassword` is **optional**. It can be used to secure any incoming requests. 481 | 482 | To get more details about the configuration have a look at the 483 | [README](https://github.com/Supereg/homebridge-http-notification-server). 484 | 485 | **Available characteristics (for the POST body)** 486 | 487 | Down here are all characteristics listed which can be updated with an request to the `homebridge-http-notification-server` 488 | 489 | * `characteristic` "On": expects a boolean `value` 490 | --------------------------------------------------------------------------------