From 0270b63e2d5b14a547fd403301008aff220c284f Mon Sep 17 00:00:00 2001 From: Bas van Rooten Date: Tue, 13 Oct 2020 01:17:18 +0200 Subject: [PATCH 1/2] v1.1 Commit. See PR --- README.md | 53 +-- app.js | 2 - auth/authentication_manager.js | 88 +++-- config/config.js | 35 +- controllers/plug_controller.js | 199 ++++++----- controllers/test_controller.js | 19 +- index.js | 8 +- package-lock.json | 613 +++++---------------------------- package.json | 3 +- routes/plug_routes.js | 15 +- routes/test_routes.js | 1 - 11 files changed, 304 insertions(+), 732 deletions(-) diff --git a/README.md b/README.md index 9940890..06c9412 100644 --- a/README.md +++ b/README.md @@ -8,28 +8,39 @@ HomeWizard Lite Proxy API, built for integrating cheap HomeWizard Lite smartplug $ npm start $ node index.js -The API depends on environment variables for authentication with HomeWizard API. - -| Key | Value | -|--|--| -| HWL_USERNAME | HomeWizard Lite App Username | -| HWL_PASSWORD | HomeWizard Lite App Password | -| LOGLEVEL | [Tracer Logger Level](https://github.com/baryon/tracer#customize-output-format) (default trace) | +The API depends on environment variables. **Make sure to configure the bold variables, as these have no default**. + +| Key | Type | Value | +|--|--|--| +| PORT | number | Port on which the API will listen (default: 3000) | +| | | | +| **HWL_USERNAME** | string | HomeWizard Lite App Username | +| **HWL_PASSWORD** | string | HomeWizard Lite App Password | +| | | | +| **SMARTPLUG_ID** | string | ID of the Internet Connected Smartplug. GET this ID from /api/smartplug | +| MIN_DIMMING_VALUE | number | The API supports dimmers. Some dimmers have a certain minimum threshold. When you request the API to go below the minimum dimming value, it'll instead turn off the light. (default: 1) | +| | | | +| CACHE_TTL | number | Default expiration time for cache objects in seconds. Currently only in use for caching session-tokens from HWL. (default: 1800) +| LOGLEVEL | string | [Tracer Logger Level](https://github.com/baryon/tracer#customize-output-format) (default: warn) | +| | | | ## Deploying -The API should be deployed on a local network, because it has no built-in authentication. It is always possible to protect the API with HTTP Basic Auth and make it accessible over the internet, but take note of the insecurities of HTTP Basic Auth. - -The API could also be deployed in a docker container - -## Requests -|Method | URL | Description | -| -- | -- | -- | -| GET | /api/test/session | Returns valid session key (Only necessary for testing)* | -| GET | /api/test/communication | Returns status 200 when API can connect to Google Test API to verify internet connectivity | -| -- | -- | -- | -| GET | /api/plug | Returns all registered plugs | -| GET | /api/act/smartplug/:smartPlugID/plug/:plugID | Returns `true` or `false` for is_active state of a plug specified by plugID | -| POST | /api/act/smartplug/:smartPlugID/plug/:plugID | Switches plug when request body contains `"action": "on"` or `"action": "off"`. Returns is_active state. - +* The API should be deployed on a local network and shouldn't be accessible over the interwebs, because it currently has no built-in authentication. It is always possible to configure [HTTP Basic Authentication](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/). +* The API should have access to homewizard.com and its subdomains. + +Using the included Dockerfile, it is also possible to run this API inside a container. + +## Endpoints +| Method | URL | Description | Example Request | Example Response | +| -- | -- | -- | -- | -- | +| GET | /api/test/session | Returns valid session key (Only necessary for testing) | | `{ "session": "string"` } | +| GET | /api/test/communication | Returns status 200 when API can connect to Google to verify internet connectivity | +| | | | +| GET | /api/plug | Returns all registered plugs | | * | +| GET | /api/smartplug | Returns info about your internet connected smartplug. (Tip: Use this to determine the ID of your smartplug. Use the `id` property.) | | * | +| GET | /api/plug/:plugID | Returns `true` or `false` for is_active state of a plug specified by plugID | | `{ "is_active": false }` | +| POST | /api/plug/:plugID | [DIMMER] Change dimmer state when request body contains `"type": "dimmer"` and `"value": number`. When value is below `MIN_DIMMING_VALUE`, the dimmer will switch off. Returns `is_active state`. | `{ "type": "dimmer", "value": 75 }` | `{ "is_active": true }` | +| | | [SWITCH] Switches plug when request body contains `"type": "switch"` and `"value": "on/off"`. Returns `is_active` state |`{ "type": "switch", "value": "on" }` | `{ "is_active": true }` | +*Try this yourself. Response contains a bunch of useful info and does not fit needly in this table. ## Contribution This project was started by an IT-student as a small 2 day project to improve home-automation by enabling Home Assistant to control HomeWizard Lite plugs. Development will continue when I need more features. Contribution is appreciated. diff --git a/app.js b/app.js index e597d79..5103a26 100644 --- a/app.js +++ b/app.js @@ -3,8 +3,6 @@ const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const morgan = require("morgan"); -const config = require('./config/config'); -const logger = require('./config/config').logger // Import models const ApiResponse = require('./models/ApiResponse'); diff --git a/auth/authentication_manager.js b/auth/authentication_manager.js index a9119a4..aba992e 100644 --- a/auth/authentication_manager.js +++ b/auth/authentication_manager.js @@ -1,56 +1,48 @@ -const logger = require('../config/config').logger -const config = require('../config/config'); -var sha1 = require('sha1'); -const axios = require('axios'); +const logger = require("../config/config").logger; +const config = require("../config/config"); +var sha1 = require("sha1"); +const axios = require("axios"); +const NodeCache = require("node-cache"); -module.exports = { +const cache = new NodeCache({ stdTTL: config.cacheTTL }); +module.exports = { // Get Session Key from HomeWizard Lite getSessionKey() { - - // Get Username and Password from config file - let username = config.hwlUsername; - let password = sha1(config.hwlPassword); - logger.debug(username); - logger.debug(password); - - // Login to HWL using user credentials - return axios({ - method: 'get', - url: 'https://cloud.homewizard.com/account/login', + // Check if sessionkey is cached + let sessionKey = cache.get("session"); + + if (sessionKey) { + logger.debug(`Cached session key found: ${sessionKey}`); + return Promise.resolve(sessionKey); + } else { + // Session key is not found in cache + let username = config.hwlUsername; + let password = sha1(config.hwlPassword); + logger.debug(username); + logger.debug(password); + + return axios({ + method: "get", + url: "https://cloud.homewizard.com/account/login", auth: { username: username, - password: password - } + password: password, + }, }) - .then(response => { - - // Check status - if (response.data.status === "ok") { - - // Authentication passed, returning session token - logger.debug(response.data.session); + .then((response) => { + logger.debug( + `Received session key: ${response.data.session}` + ); + cache.set("session", response.data.session); return response.data.session; - - } else if (response.data.status === "failed" && response.data.error === 110) { - - // Authentication failed, returning FAILED - logger.error("HWL returned invalid credentials! Check credentials for validity!") - logger.debug(response.data); - return "ERROR"; - - } else { - - // Authentication failed, but with unknown reason - logger.error("HWL returned unknown authentication error. ERROR :", response.data); - return "ERROR"; - } - }) - .catch(e => { - // Cannot communicate with HWL, returning error - logger.error("Failed to communicate with HWL! ERROR: ", e.message); - return "ERROR"; - }); - } - -} \ No newline at end of file + }) + .catch((e) => { + // Exception occured while trying to communicate with HWL. + logger.error("Can't get session key from HW"); + logger.error(e.response); + return Promise.reject("Can't get session key from HW. Check logs"); + }); + } + }, +}; diff --git a/config/config.js b/config/config.js index 531ea5e..3d0c8aa 100644 --- a/config/config.js +++ b/config/config.js @@ -1,28 +1,33 @@ // Set debug level for tracer -const loglevel = process.env.LOGLEVEL || 'trace' +const loglevel = process.env.LOGLEVEL || "warn"; module.exports = { - // Web port where the server will listen to - "webPort": process.env.PORT || 3000, - "authKey": process.env.AUTHKEY || "", + webPort: process.env.PORT || 3000, // Authentication for Homewizard Lite API - "hwlUsername": process.env.HWL_USERNAME || "", - "hwlPassword": process.env.HWL_PASSWORD || "", + hwlUsername: process.env.HWL_USERNAME || "", + hwlPassword: process.env.HWL_PASSWORD || "", + + // SmartPlugID + // Should be returned from HW when you GET on /api/smartplug + smartPlugId: + process.env.SMARTPLUG_ID || "", + // Dimmer Ranges + // Some dimmers only support dimming up to a certain amount. + minDimmingValue: process.env.MIN_DIMMING_VALUE || 1, + + // Cache Time To Live + cacheTTL: process.env.CACHE_TTL || 1800, // Tracer for logging purposes - logger: require('tracer') - .console({ - format: [ - "{{timestamp}} <{{title}}> {{file}}:{{line}} : {{message}}" - ], + logger: require("tracer").console({ + format: ["{{timestamp}} <{{title}}> {{file}}:{{line}} : {{message}}"], preprocess: function (data) { data.title = data.title.toUpperCase(); }, dateformat: "isoUtcDateTime", - level: loglevel - }) - -} \ No newline at end of file + level: loglevel, + }), +}; \ No newline at end of file diff --git a/controllers/plug_controller.js b/controllers/plug_controller.js index 86ee572..7a8c510 100644 --- a/controllers/plug_controller.js +++ b/controllers/plug_controller.js @@ -3,24 +3,17 @@ const AuthenticationManager = require('../auth/authentication_manager'); const logger = require('../config/config').logger var LocalStorage = require('node-localstorage').LocalStorage; const axios = require('axios'); +const config = require("../config/config"); localStorage = new LocalStorage('./states'); module.exports = { - - login(req, res, next) { - // TODO: Implement Login - - }, - + // Return all registered plugs getAllPlugs(req, res, next) { - // Return all registered smart plugs - - AuthenticationManager.getSessionKey().then(sessionkey => { + const sessionKey = AuthenticationManager.getSessionKey().then(sessionkey => { // Check if session key is valid - if (sessionkey != "ERROR") { - + if (sessionkey) { axios({ method: 'get', url: 'https://plug.homewizard.com/plugs', @@ -29,28 +22,19 @@ module.exports = { } }) .then(response => { - - let smartplugs = []; + let resArray = []; response.data.map((smartplug) => { - smartplugs.push(smartplug); - logger.debug(smartplug.devices); - - // Check if no plugs are attached to smartplug - if (smartplug.devices.length < 1) { - res.status(200).send(new ApiResponse("No plugs found", 200)); - } else { - // TODO: Add support for multiple SmartPlugs - // 1 Smartplug > Multiple plugs - // res.status(200).send({ - // "smartPlugID": smartplug.id, - // "smartPlugName": smartplug.name, - // "smartPlugOnline": smartplug.online, - // "devices": smartplug.devices - // }); - - res.status(200).send(smartplug.devices); - } - }) + resArray.push({ + id: smartplug.id, + identifier: smartplug.identifier, + name: smartplug.name, + latitude: smartplug.latitude, + longitude: smartplug.longitude, + online: smartplug.online, + devices: smartplug.devices, + }); + }); + res.status(200).send(resArray); }) .catch(e => { // Cannot communicate with HWL, returning error @@ -59,94 +43,124 @@ module.exports = { }); } else { // Session key is invalid - res.status(503).send(new ApiResponse("Invalid session key! Check the logs for more details about this problem ", 503)); + res.status(503).send(new ApiResponse("Invalid session key! Check logs", 503)); } + }).catch(e => { + res.status(503).send(new ApiResponse(e, 503)); }); }, + // Change the state of a plug switchPlug(req, res, next) { - AuthenticationManager.getSessionKey().then(sessionkey => { // Check if session key is valid - if (sessionkey != "ERROR") { - - axios({ - method: 'post', - url: 'https://plug.homewizard.com/plugs/' + req.params.smartPlugID + '/devices/' + req.params.plugID + '/action', - headers: { - "x-session-token": sessionkey - }, - data: { - action: req.body.action.charAt(0).toUpperCase() + req.body.action.slice(1), - } - }) - .then(response => { - logger.debug(response); - - // Check if request was successful - // Should not be necessary, but still... - - if (response.data.status === "Success") { - // Response successful - - - // Home Assistant compatibility for Restful switch, see - // https://www.home-assistant.io/components/switch.rest/ + if (sessionkey) { - // If action is ON: Return true + let data = undefined; - if (req.body.action.toUpperCase() === "ON") { + // Check plug type + if(req.body.type && req.body.type.toLowerCase() === "dimmer") { - // Add plug state to local storage to remember state for Home Assistant - localStorage.setItem(req.params.plugID, true); - logger.debug("State for " + req.params.plugID + " saved to localStorage to true"); - - res.status(200).send({ - "is_active": "true" - }); - - } else { - - localStorage.setItem(req.params.plugID, false); - logger.debug("State for " + req.params.plugID + " saved to localStorage to false"); - - res.status(200).send({ - "is_active": "false" - }); + const dimmerLevel = req.body.value; + if(typeof(dimmerLevel) === "number") { + if(dimmerLevel < config.minDimmingValue) { + data = { + action: "Off", + } + } else if (dimmerLevel <= 100){ + data = { + action: "Range", + value: dimmerLevel } - } else { - // Unsuccessful - res.status(400).send(new ApiResponse(e.message, 400)); + // Dimming level is at an invalid amount + logger.error(`Dimmer Level can't be above 100, found ${dimmerLevel}`); + res.status(400).send(new ApiResponse(`Dimmer Level can't be above 100, found ${dimmerLevel}`)); + } + } else { + res.status(400).send(new ApiResponse("Invalid value. For dimmers you should provide an integer between 0 and 100", 400)); + } + + } else { + // Plug is not a dimmer, so should be a switch + if (req.body.value && typeof(req.body.value) === "string") { + data = { + action: req.body.value.toLowerCase() === "on" ? "On" : "Off", } + } else { + res.status(400).send(new ApiResponse("Invalid value. For switches you should provide 'On' or 'Off'", 400)); + } + } + + if(data) { + axios({ + method: "post", + url: + "https://plug.homewizard.com/plugs/" + + config.smartPlugId + + "/devices/" + + req.params.plugID + + "/action", + headers: { + "x-session-token": sessionkey, + }, + data: data, }) - .catch(e => { - // Cannot communicate with HWL, returning error - logger.error("HWL declined the request. ERROR: ", e.message); - res.status(400).send(new ApiResponse("HWL declined the request. ERROR: " + e.message, 400)); - }); + .then((response) => { + logger.debug(response); + + // Check if request was successful + // Should not be necessary, but still... + + if (response.data.status === "Success") { + // Response successful + + // Home Assistant compatibility for Restful switch, see + // https://www.home-assistant.io/components/switch.rest/ + + if(typeof(req.body.value) === "number") { + const plugState = req.body.value >= config.minDimmingValue; + localStorage.setItem(req.params.plugID, plugState); + res.status(200).send({ is_active: plugState }); + } else { + const plugState = req.body.value.toLowerCase() === "on"; + localStorage.setItem(req.params.plugID, plugState); + res.status(200).send({ is_active: plugState }); + } + } else { + // Unsuccessful + res.status(503).send(new ApiResponse(`HWL returned an error. Check logs`, 503)); + } + }) + .catch((e) => { + // Cannot communicate with HWL, returning error + logger.error(`Can't communicate with HWL: ${e}`); + res.status(400).send(new ApiResponse(`Can't communicate with HWL. Check logs`, 503)); + }); + } } else { // Session key is invalid res.status(503).send(new ApiResponse("Invalid session key! Check the logs for more details about this problem ", 503)); } + }).catch(e => { + logger.error(e); + res.status(503).send(new ApiResponse(e, 503)); }); }, + // Get state of plug plugState(req, res, next) { - // Check if state exists if (!localStorage.getItem(req.params.plugID)) { logger.debug("Item doesn't exist"); - + // Return false if plug doesn't have a recorded state res.status(200).send({ "is_active": "false" }); - } else { logger.debug("Item exists"); - // Return state from localStorage res.status(200).send({ "is_active": localStorage.getItem(req.params.plugID) @@ -154,14 +168,12 @@ module.exports = { } }, + // Get information about Smartplug getSmartPlug(req, res, next) { - // Return Smartplug details - AuthenticationManager.getSessionKey().then(sessionkey => { // Check if session key is valid - if (sessionkey != "ERROR") { - + if (sessionkey) { axios({ method: 'get', url: 'https://plug.homewizard.com/plugs', @@ -199,6 +211,9 @@ module.exports = { // Session key is invalid res.status(503).send(new ApiResponse("Invalid session key! Check the logs for more details about this problem ", 503)); } + }).catch(e => { + logger.error(e); + res.status(503).send(new ApiResponse(e, 503)); }); } } \ No newline at end of file diff --git a/controllers/test_controller.js b/controllers/test_controller.js index 56f8c27..f44ef02 100644 --- a/controllers/test_controller.js +++ b/controllers/test_controller.js @@ -6,21 +6,14 @@ const axios = require('axios'); module.exports = { - responseTest(req, res, next) { - - res.status(200).send(new ApiResponse("Acknowledged", 200)); - logger.debug("Returned test message"); - }, - communicationTest(req, res, next) { - axios.get('https://clients3.google.com/generate_204') - .then(result => { - logger.debug(result); + axios.get("https://www.google.com") + .then(() => { res.status(200).send(new ApiResponse("Successfully connected to the internet", 200)); }).catch(e => { - logger.error("Can't connect to Google Test API, possible internet outtage? ERROR: ", e.message); - res.status(503).send(new ApiResponse(e.message, 503)); - }) + logger.error("Can't connect to Google, possible internet outtage? Check log", e.message); + res.status(503).send(new ApiResponse("Can't connect to Google, possible internet outtage?", 503)); + }); }, getSessionKey(req, res, next) { @@ -28,6 +21,8 @@ module.exports = { res.status(200).send({ "session": sessionkey }); + }).catch(e => { + res.status(503).send(new ApiResponse(e, 503)); }); } } \ No newline at end of file diff --git a/index.js b/index.js index 1d55cd1..bc07d57 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,7 @@ const app = require('./app'); const config = require('./config/config'); -let port = config.webPort; - -//Ensures the app starts and uses port 3000 or environment port -app.listen(port, () => { - console.info('App running on port ' + port) +// Ensures the app starts and uses port 3000 or environment port +app.listen(config.webPort, () => { + console.info('App running on port ' + config.webPort); }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7178c40..1ecb6a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -182,6 +182,15 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "body-parser": { "version": "1.18.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", @@ -296,9 +305,9 @@ "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" }, "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "requires": { "anymatch": "^2.0.0", "async-each": "^1.0.1", @@ -345,6 +354,11 @@ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=" }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -383,11 +397,11 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "configstore": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", - "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz", + "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", "requires": { - "dot-prop": "^4.1.0", + "dot-prop": "^4.2.1", "graceful-fs": "^4.1.2", "make-dir": "^1.0.0", "unique-string": "^1.0.0", @@ -533,9 +547,9 @@ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "dot-prop": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", - "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", "requires": { "is-obj": "^1.0.0" } @@ -731,6 +745,12 @@ } } }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "optional": true + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -808,484 +828,13 @@ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "optional": true, "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "optional": true - } + "bindings": "^1.5.0", + "nan": "^2.12.1" } }, "get-stream": { @@ -1587,9 +1136,9 @@ "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" }, "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==" }, "is-stream": { "version": "1.1.0", @@ -1617,9 +1166,9 @@ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "latest-version": { "version": "3.1.0", @@ -1775,9 +1324,9 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", "optional": true }, "nanomatch": { @@ -1803,6 +1352,14 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" }, + "node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "requires": { + "clone": "2.x" + } + }, "node-localstorage": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-localstorage/-/node-localstorage-1.3.1.tgz", @@ -1824,17 +1381,17 @@ } }, "nodemon": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.1.tgz", - "integrity": "sha512-/DXLzd/GhiaDXXbGId5BzxP1GlsqtMGM9zTmkWrgXtSqjKmGSbLicM/oAy4FR0YWm14jCHRwnR31AHS2dYFHrg==", + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.19.4.tgz", + "integrity": "sha512-VGPaqQBNk193lrJFotBU8nvWZPqEZY2eIzymy2jjY0fJ9qIsxA0sxQ8ATPl0gZC645gijYEc1jtZvpS8QWzJGQ==", "requires": { - "chokidar": "^2.1.5", - "debug": "^3.1.0", + "chokidar": "^2.1.8", + "debug": "^3.2.6", "ignore-by-default": "^1.0.1", "minimatch": "^3.0.4", - "pstree.remy": "^1.1.6", - "semver": "^5.5.0", - "supports-color": "^5.2.0", + "pstree.remy": "^1.1.7", + "semver": "^5.7.1", + "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.2", "update-notifier": "^2.5.0" @@ -2024,9 +1581,9 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "pstree.remy": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.7.tgz", - "integrity": "sha512-xsMgrUwRpuGskEzBFkH8NmTimbZ5PcPup0LA8JJkHIm2IMUbQcpo3yeLNWVrufEYjh8YwtSVh0xz6UeWc5Oh5A==" + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==" }, "qs": { "version": "6.5.2", @@ -2061,16 +1618,16 @@ }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" } } }, "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2161,9 +1718,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-diff": { "version": "2.1.0", @@ -2253,9 +1810,9 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "slide": { "version": "1.1.6", @@ -2365,11 +1922,11 @@ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "requires": { - "atob": "^2.1.1", + "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", @@ -2541,9 +2098,9 @@ } }, "undefsafe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.2.tgz", - "integrity": "sha1-Il9rngM3Zj4Njnz9aG/Cg2zKznY=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", + "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", "requires": { "debug": "^2.2.0" } @@ -2614,9 +2171,9 @@ "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=" }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" }, "update-notifier": { "version": "2.5.0", diff --git a/package.json b/package.json index 075a7e2..493473c 100644 --- a/package.json +++ b/package.json @@ -23,8 +23,9 @@ "cors": "^2.8.5", "express": "^4.16.4", "morgan": "^1.9.1", + "node-cache": "^5.1.2", "node-localstorage": "^1.3.1", - "nodemon": "^1.19.1", + "nodemon": "^1.19.4", "sha1": "^1.1.1", "tracer": "^0.9.8" } diff --git a/routes/plug_routes.js b/routes/plug_routes.js index 78ac2bb..49556e2 100644 --- a/routes/plug_routes.js +++ b/routes/plug_routes.js @@ -1,10 +1,11 @@ -let routes = require('express').Router(); -const PlugController = require('../controllers/plug_controller'); +let routes = require("express").Router(); +const PlugController = require("../controllers/plug_controller"); // Plug routes -routes.get('/plug', PlugController.getAllPlugs); -routes.get('/smartplug', PlugController.getSmartPlug); -routes.post('/act/smartplug/:smartPlugID/plug/:plugID', PlugController.switchPlug); -routes.get('/act/smartplug/:smartPlugID/plug/:plugID', PlugController.plugState); +// Check documentation for possible calls +routes.get("/plug", PlugController.getAllPlugs); +routes.get("/smartplug", PlugController.getSmartPlug); +routes.post("/plug/:plugID", PlugController.switchPlug); +routes.get("/plug/:plugID", PlugController.plugState); -module.exports = routes; \ No newline at end of file +module.exports = routes; diff --git a/routes/test_routes.js b/routes/test_routes.js index b67b5b4..060dbd0 100644 --- a/routes/test_routes.js +++ b/routes/test_routes.js @@ -2,7 +2,6 @@ let routes = require('express').Router(); let TestController = require('../controllers/test_controller'); // Test Routes -routes.get('/test', TestController.responseTest); routes.get('/test/communication', TestController.communicationTest); routes.get('/test/session', TestController.getSessionKey); From 32a8fc567d136ea91176b8f5e88f09fd34fae1fb Mon Sep 17 00:00:00 2001 From: Bas van Rooten <33634284+basvanrooten@users.noreply.github.com> Date: Tue, 13 Oct 2020 01:34:45 +0200 Subject: [PATCH 2/2] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 06c9412..360d288 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Using the included Dockerfile, it is also possible to run this API inside a cont | GET | /api/plug/:plugID | Returns `true` or `false` for is_active state of a plug specified by plugID | | `{ "is_active": false }` | | POST | /api/plug/:plugID | [DIMMER] Change dimmer state when request body contains `"type": "dimmer"` and `"value": number`. When value is below `MIN_DIMMING_VALUE`, the dimmer will switch off. Returns `is_active state`. | `{ "type": "dimmer", "value": 75 }` | `{ "is_active": true }` | | | | [SWITCH] Switches plug when request body contains `"type": "switch"` and `"value": "on/off"`. Returns `is_active` state |`{ "type": "switch", "value": "on" }` | `{ "is_active": true }` | -*Try this yourself. Response contains a bunch of useful info and does not fit needly in this table. + +`*` Try this yourself. Response contains a bunch of useful info and does not fit needly in this table. ## Contribution This project was started by an IT-student as a small 2 day project to improve home-automation by enabling Home Assistant to control HomeWizard Lite plugs. Development will continue when I need more features. Contribution is appreciated.