├── .gitignore ├── .npmignore ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── README.md ├── certs ├── server.crt └── server.key ├── package-lock.json ├── package.json ├── sonoff.config.json ├── sonoff.server.js ├── sonoff.server.module.js └── sonoff.setupdevice.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}\\sonoff.server.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM arm32v7/node:9.3-slim 2 | 3 | ENV HTTP_PORT=8080 4 | ENV HTTPS_PORT=8443 5 | ENV WEBSOCKET_PORT=9443 6 | 7 | WORKDIR simple-sonoff-server 8 | ADD ./ ./ 9 | 10 | RUN npm install 11 | CMD node sonoff.server.js 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple SONOFF Server 2 | 3 | **DISCONTINUED due to certification problems on newer firmwares. See [this issue](https://github.com/mdopp/simple-sonoff-server/issues/28) for details.** 4 | 5 | Current State => Not finished yet, but works most of the time. 6 | 7 | A lot of this code is based on the findings in this blog 8 | http://blog.nanl.de/2017/05/sonota-flashing-itead-sonoff-devices-via-original-ota-mechanism/ 9 | and 10 | https://blog.ipsumdomus.com/sonoff-switch-complete-hack-without-firmware-upgrade-1b2d6632c01 11 | 12 | The idea was to have an Openhab Binding. And this is the concept implementation, that works good enough for me to start with. It can be used in combination with the HTTP Binding. 13 | 14 | # Configuration 15 | 16 | I suggest to use => https://github.com/saryn/node-red-contrib-sonoff-server 17 | It solves a lot of problems like taking care of starting/stoping the server or keeping it running. 18 | 19 | If you still want to use this directly: 20 | 21 | Change the sonoff.config.json to fit your environment. 22 | 23 | The "server" is the device, which should stay in contact with the SONOFF devices. In my case it was the Raspverry Pi, which also runs Openhab. 24 | 25 | * "httpsPort" can be any port. 26 | * "websocketPort" can be any port. 27 | 28 | But make sure, that your router is allowing communication between devices. 29 | 30 | ```json 31 | { 32 | "router": { 33 | "SSID": "##########", 34 | "password": "###########" 35 | }, 36 | "server": { 37 | "IP": "0.0.0.0", 38 | "httpPort": 1080, 39 | "httpsPort": 1081, 40 | "websocketPort": 443 41 | } 42 | } 43 | ``` 44 | 45 | # Setup a new device 46 | There are two ways of setting up the devices. Either with the sonoff.setupdevice.js script, or with wget (or anything else that can post an http request to a specific server). 47 | 48 | ### sonoff.setupdevice.js 49 | Start sonoff.setupdevice.js on a computer you like. It will connect to the SONOFF device, so you will lose internet connection. When the scripts runs, you must long-click the black button on the device, and it will be configured to use the "server" as its cloud. Which now runs in your own network. 50 | 51 | To run this on a linux device, the network manager must be installed. On an raspberry pi I would suggest to do the setup process manually with wget. 52 | 53 | ### wget 54 | (thanks @andrewerrington) 55 | 1. Put the SonOff/Wemos device in AP mode (press and hold button for 5s) 56 | 1. Find the device and connect to it (SSID: ITEAD-10000xxxxx Password: 12345678) 57 | 1. Add route if necessary `sudo route change 0.0.0.0 mask 0.0.0.0 10.10.7.1` 58 | 1. (optional) use wget to read device info `wget -O- 10.10.7.1/device` 59 | 1. use wget to send local WiFi settings to device `wget -O- --post-data='{"version":4,"ssid":"yourSSID","password":"yourSSID_PASSWORD","serverName":"n.n.n.n","port":1081}' --header=Content-Type:application/json "http://10.10.7.1/ap"` 60 | 61 | The device will automatically drop out of AP mode and tries to connect to WiFi and server. 62 | 63 | # running the server 64 | Start sonoff.server.js 65 | This Server keeps the connection to the sonoff devices, and must run permanently. 66 | 67 | * /devices => list off all devices that are currently known to the server. 68 | * /devices/:deviceId/status => shows the status of the device 69 | * /devices/:deviceId/on => turns the device "on" 70 | * /devices/:deviceId/off => turns the device "off" 71 | -------------------------------------------------------------------------------- /certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDTjCCAjYCCQDnDHmAZM02TjANBgkqhkiG9w0BAQUFADBpMQswCQYDVQQGEwJH 3 | QjEPMA0GA1UECAwGU3RyZWV0MQ0wCwYDVQQHDARDaXR5MRUwEwYDVQQKDAxPcmdh 4 | bmlzYXRpb24xEjAQBgNVBAsMCUF1dGhvcml0eTEPMA0GA1UEAwwGY2hyaXNlMB4X 5 | DTE3MTIzMTEwMTEyNFoXDTIwMTAyMDEwMTEyNFowaTELMAkGA1UEBhMCR0IxDzAN 6 | BgNVBAgMBlN0cmVldDENMAsGA1UEBwwEQ2l0eTEVMBMGA1UECgwMT3JnYW5pc2F0 7 | aW9uMRIwEAYDVQQLDAlBdXRob3JpdHkxDzANBgNVBAMMBmNocmlzZTCCASIwDQYJ 8 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBALVzg1I44y0RBM12wa6MP7hQrah6fzl6 9 | pdhI6zEGi6i4h3R2xbnha7swWb7QN9Ud/upwnFIwBNJ9GYm9+qvmX2tNrQSlCX3n 10 | DoBYrTL1jWLvNY/RuylpKLU84fFWqOx6bi3+k/BNpEo76EaZ06B4wdwB4Xsjp0SF 11 | dRm6sYcKnHl1gaZFgxMAQNdCpf7R3f3enKmr3kSvEKbmFEIK6LNZNht1xF5MUj0H 12 | 7ax5u50+F9bjru9qn70yhfv5fjXmdbKKsjFKhM7VhV8jFV9ZGmyxN9ym4JFAq1ea 13 | 5DZ4HZaSRP7MPbhtT9qjmTTPD+cBHFCIhGspZwyOKErG5Z7aFfm+UTcCAwEAATAN 14 | BgkqhkiG9w0BAQUFAAOCAQEAfNkwmXECYW1GuBHqOz4uQn4xozvsAX8xDr9JiHfJ 15 | iOio8PrTdHfapWT4hGQXb0EecNE8gkhaH2Y/4yX1dEvr1cOtLH12wjXFHqwP2kSS 16 | rIdNsgYpbmpndQyTjdH8rEVhYFzFeC1nDazGk1aI7BJa/5RO43a31QKUdGUjmGB2 17 | nHUlcS6TL1EuWGns3bS0wMwtCXZEswjiDp/XU3j0NycC+EdpXUhQH93eVL+JBF6R 18 | FTERx65b1qZ43raWRDOu6v75c8vjQmc7Bk2zKDYrZQlxzskXwuIISe2Bn0yJh1rZ 19 | IGE4cr6XF45C+HnXT3Vd/QG/ye2rJUDrFUSMiQUGpN/xBw== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1c4NSOOMtEQTN 3 | dsGujD+4UK2oen85eqXYSOsxBououId0dsW54Wu7MFm+0DfVHf7qcJxSMATSfRmJ 4 | vfqr5l9rTa0EpQl95w6AWK0y9Y1i7zWP0bspaSi1POHxVqjsem4t/pPwTaRKO+hG 5 | mdOgeMHcAeF7I6dEhXUZurGHCpx5dYGmRYMTAEDXQqX+0d393pypq95ErxCm5hRC 6 | CuizWTYbdcReTFI9B+2sebudPhfW467vap+9MoX7+X415nWyirIxSoTO1YVfIxVf 7 | WRpssTfcpuCRQKtXmuQ2eB2WkkT+zD24bU/ao5k0zw/nARxQiIRrKWcMjihKxuWe 8 | 2hX5vlE3AgMBAAECggEBAIIMP1Of2BqFtxZ3hgtPE6BLmptE6IhOwiyI3qAtm80a 9 | LGMpeT9nM+pelgPvggSp0fk/KpqxNdOSu8oAMngfzcW/T0Ej8/CbuMX+SGxcl7AP 10 | 2Ciy3dypNPboottxC/MpMGygqHEzhIt78lcfixDZW8AOTQgPnqO6rxn4t6Ic1dcq 11 | TSdRmw9idkpH1jlLKmrDMRySW4PHRbtQQecZSnc7vl7PADvfYKgQzeHN/TgdJyWp 12 | eWTf+yFVdZhuWkCBicBa1p7liJZBwkHIxWy43k+9s+/aQjjdT8ZpFbqOG7aKdSJm 13 | gzcSLjZNEIzDNm9MibN7dzXLwqgyhbK26uVuqctReHECgYEA8JyKuCZwWT23MRd3 14 | bEKzxEjxj/xfbs3pSb5mDLDcbjc0md7oqvNgLxC/deF4pTkCthnzGMLdcPjosunZ 15 | SUC/aikAqpZpx8EKXvEPVWbhUKhT7DzjMIKOJ1zRGNYYiwgTo2FHGHyN+Q0ClVFZ 16 | MMJ8tMG1xB+wEpmjuCCV8YotTk8CgYEAwQ5bsMj8a1vspvqsClSzaS3lm1/Yl51M 17 | GGf476Y3AohO4kRmE27cboD6mF5Qrow9GJAW8dqgIDk8MhD3oK+0bc8X8rFbuEdl 18 | Ij+1m0VkzFI0zbx4rv9RDKSGaKI0TczoyNUpGQ52HygDOYi0290cYaHnQ8mMp3/E 19 | Mx8QoxCwPJkCgYBp1rI+ZsostzUJ5ySk/oJM6Tk4+O6RswnXYmVJV9LeuHHu7Vqz 20 | N/MrQNa3Z8LGCtnieG+yiKUXuKXDbZIi4tRQQlEEuL03HPcO5EK2clbSP/kKhY36 21 | HLOjJPTlU+ff4GGjEHtRjhh1YP+t5uawyBxK9pqZ9sZgqCyYJd1PLlTXiQKBgDqt 22 | 8pC5hn/bYQMgDkrVk/LUUIX5jAwJc6e3cqLlErNiWxfLUnt1puqvqXvGrx9e4PMm 23 | WNMKcMFapEFThvEAdwcpe035Ore4e7T9SGX5obm5AvPF5ajLUgdVusTzCJLdRaRF 24 | pkhHbP/rWY+TTeXGP2otm+2uxWHkI+6l8pid6fOBAoGAQZXVkngz32JhYlAvbf2p 25 | UQxc6PZE0wqnf24A3sam8/2GFVhfgrg4Z6m2nTJFrsszdvypArVOA+YYUm/CQegx 26 | rNiIYX7nDIwo7bug0cHdZko3PHHl1D7XtRKcdexk5s2iN3K8h19qdnXWz9wZ9lzw 27 | VuuaAYHoPEj+qlVuAvTiD1E= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-sonoff-server", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.4", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.4.tgz", 10 | "integrity": "sha1-hiRnWMfdbSGmR0/whKR0DsBesh8=", 11 | "requires": { 12 | "mime-types": "2.1.17", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "ajv": { 17 | "version": "5.5.2", 18 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", 19 | "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", 20 | "requires": { 21 | "co": "4.6.0", 22 | "fast-deep-equal": "1.0.0", 23 | "fast-json-stable-stringify": "2.0.0", 24 | "json-schema-traverse": "0.3.1" 25 | } 26 | }, 27 | "array-flatten": { 28 | "version": "1.1.1", 29 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 30 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 31 | }, 32 | "asn1": { 33 | "version": "0.2.3", 34 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", 35 | "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" 36 | }, 37 | "assert-plus": { 38 | "version": "1.0.0", 39 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 40 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 41 | }, 42 | "asynckit": { 43 | "version": "0.4.0", 44 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 45 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 46 | }, 47 | "aws-sign2": { 48 | "version": "0.7.0", 49 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 50 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 51 | }, 52 | "aws4": { 53 | "version": "1.6.0", 54 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", 55 | "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" 56 | }, 57 | "bcrypt-pbkdf": { 58 | "version": "1.0.1", 59 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", 60 | "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", 61 | "optional": true, 62 | "requires": { 63 | "tweetnacl": "0.14.5" 64 | } 65 | }, 66 | "body-parser": { 67 | "version": "1.18.2", 68 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 69 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 70 | "requires": { 71 | "bytes": "3.0.0", 72 | "content-type": "1.0.4", 73 | "debug": "2.6.9", 74 | "depd": "1.1.1", 75 | "http-errors": "1.6.2", 76 | "iconv-lite": "0.4.19", 77 | "on-finished": "2.3.0", 78 | "qs": "6.5.1", 79 | "raw-body": "2.3.2", 80 | "type-is": "1.6.15" 81 | } 82 | }, 83 | "boom": { 84 | "version": "4.3.1", 85 | "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", 86 | "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", 87 | "requires": { 88 | "hoek": "4.2.0" 89 | } 90 | }, 91 | "bytes": { 92 | "version": "3.0.0", 93 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 94 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 95 | }, 96 | "caseless": { 97 | "version": "0.12.0", 98 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 99 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 100 | }, 101 | "co": { 102 | "version": "4.6.0", 103 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 104 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 105 | }, 106 | "combined-stream": { 107 | "version": "1.0.5", 108 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", 109 | "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", 110 | "requires": { 111 | "delayed-stream": "1.0.0" 112 | } 113 | }, 114 | "content-disposition": { 115 | "version": "0.5.2", 116 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 117 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 118 | }, 119 | "content-type": { 120 | "version": "1.0.4", 121 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 122 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 123 | }, 124 | "cookie": { 125 | "version": "0.3.1", 126 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 127 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 128 | }, 129 | "cookie-signature": { 130 | "version": "1.0.6", 131 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 132 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 133 | }, 134 | "core-util-is": { 135 | "version": "1.0.2", 136 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 137 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 138 | }, 139 | "cryptiles": { 140 | "version": "3.1.2", 141 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", 142 | "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", 143 | "requires": { 144 | "boom": "5.2.0" 145 | }, 146 | "dependencies": { 147 | "boom": { 148 | "version": "5.2.0", 149 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", 150 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", 151 | "requires": { 152 | "hoek": "4.2.0" 153 | } 154 | } 155 | } 156 | }, 157 | "dashdash": { 158 | "version": "1.14.1", 159 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 160 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 161 | "requires": { 162 | "assert-plus": "1.0.0" 163 | } 164 | }, 165 | "debug": { 166 | "version": "2.6.9", 167 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 168 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 169 | "requires": { 170 | "ms": "2.0.0" 171 | } 172 | }, 173 | "delayed-stream": { 174 | "version": "1.0.0", 175 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 176 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 177 | }, 178 | "depd": { 179 | "version": "1.1.1", 180 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 181 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 182 | }, 183 | "destroy": { 184 | "version": "1.0.4", 185 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 186 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 187 | }, 188 | "ecc-jsbn": { 189 | "version": "0.1.1", 190 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", 191 | "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", 192 | "optional": true, 193 | "requires": { 194 | "jsbn": "0.1.1" 195 | } 196 | }, 197 | "ee-first": { 198 | "version": "1.1.1", 199 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 200 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 201 | }, 202 | "encodeurl": { 203 | "version": "1.0.1", 204 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.1.tgz", 205 | "integrity": "sha1-eePVhlU0aQn+bw9Fpd5oEDspTSA=" 206 | }, 207 | "escape-html": { 208 | "version": "1.0.3", 209 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 210 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 211 | }, 212 | "etag": { 213 | "version": "1.8.1", 214 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 215 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 216 | }, 217 | "express": { 218 | "version": "4.16.2", 219 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", 220 | "integrity": "sha1-41xt/i1kt9ygpc1PIXgb4ymeB2w=", 221 | "requires": { 222 | "accepts": "1.3.4", 223 | "array-flatten": "1.1.1", 224 | "body-parser": "1.18.2", 225 | "content-disposition": "0.5.2", 226 | "content-type": "1.0.4", 227 | "cookie": "0.3.1", 228 | "cookie-signature": "1.0.6", 229 | "debug": "2.6.9", 230 | "depd": "1.1.1", 231 | "encodeurl": "1.0.1", 232 | "escape-html": "1.0.3", 233 | "etag": "1.8.1", 234 | "finalhandler": "1.1.0", 235 | "fresh": "0.5.2", 236 | "merge-descriptors": "1.0.1", 237 | "methods": "1.1.2", 238 | "on-finished": "2.3.0", 239 | "parseurl": "1.3.2", 240 | "path-to-regexp": "0.1.7", 241 | "proxy-addr": "2.0.2", 242 | "qs": "6.5.1", 243 | "range-parser": "1.2.0", 244 | "safe-buffer": "5.1.1", 245 | "send": "0.16.1", 246 | "serve-static": "1.13.1", 247 | "setprototypeof": "1.1.0", 248 | "statuses": "1.3.1", 249 | "type-is": "1.6.15", 250 | "utils-merge": "1.0.1", 251 | "vary": "1.1.2" 252 | } 253 | }, 254 | "extend": { 255 | "version": "3.0.1", 256 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", 257 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" 258 | }, 259 | "extsprintf": { 260 | "version": "1.3.0", 261 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 262 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 263 | }, 264 | "fast-deep-equal": { 265 | "version": "1.0.0", 266 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", 267 | "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" 268 | }, 269 | "fast-json-stable-stringify": { 270 | "version": "2.0.0", 271 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 272 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 273 | }, 274 | "finalhandler": { 275 | "version": "1.1.0", 276 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", 277 | "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=", 278 | "requires": { 279 | "debug": "2.6.9", 280 | "encodeurl": "1.0.1", 281 | "escape-html": "1.0.3", 282 | "on-finished": "2.3.0", 283 | "parseurl": "1.3.2", 284 | "statuses": "1.3.1", 285 | "unpipe": "1.0.0" 286 | } 287 | }, 288 | "forever-agent": { 289 | "version": "0.6.1", 290 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 291 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 292 | }, 293 | "form-data": { 294 | "version": "2.3.1", 295 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", 296 | "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", 297 | "requires": { 298 | "asynckit": "0.4.0", 299 | "combined-stream": "1.0.5", 300 | "mime-types": "2.1.17" 301 | } 302 | }, 303 | "forwarded": { 304 | "version": "0.1.2", 305 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 306 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 307 | }, 308 | "fresh": { 309 | "version": "0.5.2", 310 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 311 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 312 | }, 313 | "fs": { 314 | "version": "0.0.1-security", 315 | "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", 316 | "integrity": "sha1-invTcYa23d84E/I4WLV+yq9eQdQ=" 317 | }, 318 | "getpass": { 319 | "version": "0.1.7", 320 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 321 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 322 | "requires": { 323 | "assert-plus": "1.0.0" 324 | } 325 | }, 326 | "har-schema": { 327 | "version": "2.0.0", 328 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 329 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 330 | }, 331 | "har-validator": { 332 | "version": "5.0.3", 333 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", 334 | "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", 335 | "requires": { 336 | "ajv": "5.5.2", 337 | "har-schema": "2.0.0" 338 | } 339 | }, 340 | "hawk": { 341 | "version": "6.0.2", 342 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", 343 | "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", 344 | "requires": { 345 | "boom": "4.3.1", 346 | "cryptiles": "3.1.2", 347 | "hoek": "4.2.0", 348 | "sntp": "2.1.0" 349 | } 350 | }, 351 | "hoek": { 352 | "version": "4.2.0", 353 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", 354 | "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" 355 | }, 356 | "http": { 357 | "version": "0.0.0", 358 | "resolved": "https://registry.npmjs.org/http/-/http-0.0.0.tgz", 359 | "integrity": "sha1-huYybSnF0Dnen6xYSkVon5KfT3I=" 360 | }, 361 | "http-errors": { 362 | "version": "1.6.2", 363 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 364 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 365 | "requires": { 366 | "depd": "1.1.1", 367 | "inherits": "2.0.3", 368 | "setprototypeof": "1.0.3", 369 | "statuses": "1.3.1" 370 | }, 371 | "dependencies": { 372 | "setprototypeof": { 373 | "version": "1.0.3", 374 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 375 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 376 | } 377 | } 378 | }, 379 | "http-signature": { 380 | "version": "1.2.0", 381 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 382 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 383 | "requires": { 384 | "assert-plus": "1.0.0", 385 | "jsprim": "1.4.1", 386 | "sshpk": "1.13.1" 387 | } 388 | }, 389 | "https": { 390 | "version": "1.0.0", 391 | "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", 392 | "integrity": "sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=" 393 | }, 394 | "iconv-lite": { 395 | "version": "0.4.19", 396 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 397 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 398 | }, 399 | "inherits": { 400 | "version": "2.0.3", 401 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 402 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 403 | }, 404 | "invert-kv": { 405 | "version": "1.0.0", 406 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", 407 | "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" 408 | }, 409 | "ipaddr.js": { 410 | "version": "1.5.2", 411 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz", 412 | "integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A=" 413 | }, 414 | "is-typedarray": { 415 | "version": "1.0.0", 416 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 417 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 418 | }, 419 | "isstream": { 420 | "version": "0.1.2", 421 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 422 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 423 | }, 424 | "jsbn": { 425 | "version": "0.1.1", 426 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 427 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 428 | "optional": true 429 | }, 430 | "json-schema": { 431 | "version": "0.2.3", 432 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 433 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 434 | }, 435 | "json-schema-traverse": { 436 | "version": "0.3.1", 437 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 438 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" 439 | }, 440 | "json-stringify-safe": { 441 | "version": "5.0.1", 442 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 443 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 444 | }, 445 | "jsprim": { 446 | "version": "1.4.1", 447 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 448 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 449 | "requires": { 450 | "assert-plus": "1.0.0", 451 | "extsprintf": "1.3.0", 452 | "json-schema": "0.2.3", 453 | "verror": "1.10.0" 454 | } 455 | }, 456 | "lcid": { 457 | "version": "1.0.0", 458 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", 459 | "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", 460 | "requires": { 461 | "invert-kv": "1.0.0" 462 | } 463 | }, 464 | "media-typer": { 465 | "version": "0.3.0", 466 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 467 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 468 | }, 469 | "merge-descriptors": { 470 | "version": "1.0.1", 471 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 472 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 473 | }, 474 | "methods": { 475 | "version": "1.1.2", 476 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 477 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 478 | }, 479 | "mime": { 480 | "version": "1.4.1", 481 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 482 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 483 | }, 484 | "mime-db": { 485 | "version": "1.30.0", 486 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", 487 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" 488 | }, 489 | "mime-types": { 490 | "version": "2.1.17", 491 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", 492 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", 493 | "requires": { 494 | "mime-db": "1.30.0" 495 | } 496 | }, 497 | "ms": { 498 | "version": "2.0.0", 499 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 500 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 501 | }, 502 | "negotiator": { 503 | "version": "0.6.1", 504 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 505 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 506 | }, 507 | "node-wifiscanner2": { 508 | "version": "1.2.0", 509 | "resolved": "https://registry.npmjs.org/node-wifiscanner2/-/node-wifiscanner2-1.2.0.tgz", 510 | "integrity": "sha1-ln+tDXV7Qwo5snGNQIy72ewetIA=", 511 | "requires": { 512 | "os-locale": "1.4.0" 513 | } 514 | }, 515 | "nodejs-websocket": { 516 | "version": "1.7.1", 517 | "resolved": "https://registry.npmjs.org/nodejs-websocket/-/nodejs-websocket-1.7.1.tgz", 518 | "integrity": "sha1-zM+7qCO/HPqWgPFoq3q1MSHkhBA=" 519 | }, 520 | "oauth-sign": { 521 | "version": "0.8.2", 522 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", 523 | "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" 524 | }, 525 | "on-finished": { 526 | "version": "2.3.0", 527 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 528 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 529 | "requires": { 530 | "ee-first": "1.1.1" 531 | } 532 | }, 533 | "os-locale": { 534 | "version": "1.4.0", 535 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", 536 | "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", 537 | "requires": { 538 | "lcid": "1.0.0" 539 | } 540 | }, 541 | "parseurl": { 542 | "version": "1.3.2", 543 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 544 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 545 | }, 546 | "path-to-regexp": { 547 | "version": "0.1.7", 548 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 549 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 550 | }, 551 | "performance-now": { 552 | "version": "2.1.0", 553 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 554 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 555 | }, 556 | "proxy-addr": { 557 | "version": "2.0.2", 558 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", 559 | "integrity": "sha1-ZXFQT0e7mI7IGAJT+F3X4UlSvew=", 560 | "requires": { 561 | "forwarded": "0.1.2", 562 | "ipaddr.js": "1.5.2" 563 | } 564 | }, 565 | "punycode": { 566 | "version": "1.4.1", 567 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 568 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 569 | }, 570 | "qs": { 571 | "version": "6.5.1", 572 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 573 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 574 | }, 575 | "querystring": { 576 | "version": "0.2.0", 577 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 578 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 579 | }, 580 | "range-parser": { 581 | "version": "1.2.0", 582 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 583 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 584 | }, 585 | "raw-body": { 586 | "version": "2.3.2", 587 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 588 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 589 | "requires": { 590 | "bytes": "3.0.0", 591 | "http-errors": "1.6.2", 592 | "iconv-lite": "0.4.19", 593 | "unpipe": "1.0.0" 594 | } 595 | }, 596 | "request": { 597 | "version": "2.83.0", 598 | "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", 599 | "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", 600 | "requires": { 601 | "aws-sign2": "0.7.0", 602 | "aws4": "1.6.0", 603 | "caseless": "0.12.0", 604 | "combined-stream": "1.0.5", 605 | "extend": "3.0.1", 606 | "forever-agent": "0.6.1", 607 | "form-data": "2.3.1", 608 | "har-validator": "5.0.3", 609 | "hawk": "6.0.2", 610 | "http-signature": "1.2.0", 611 | "is-typedarray": "1.0.0", 612 | "isstream": "0.1.2", 613 | "json-stringify-safe": "5.0.1", 614 | "mime-types": "2.1.17", 615 | "oauth-sign": "0.8.2", 616 | "performance-now": "2.1.0", 617 | "qs": "6.5.1", 618 | "safe-buffer": "5.1.1", 619 | "stringstream": "0.0.5", 620 | "tough-cookie": "2.3.3", 621 | "tunnel-agent": "0.6.0", 622 | "uuid": "3.1.0" 623 | } 624 | }, 625 | "safe-buffer": { 626 | "version": "5.1.1", 627 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 628 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 629 | }, 630 | "send": { 631 | "version": "0.16.1", 632 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.1.tgz", 633 | "integrity": "sha512-ElCLJdJIKPk6ux/Hocwhk7NFHpI3pVm/IZOYWqUmoxcgeyM+MpxHHKhb8QmlJDX1pU6WrgaHBkVNm73Sv7uc2A==", 634 | "requires": { 635 | "debug": "2.6.9", 636 | "depd": "1.1.1", 637 | "destroy": "1.0.4", 638 | "encodeurl": "1.0.1", 639 | "escape-html": "1.0.3", 640 | "etag": "1.8.1", 641 | "fresh": "0.5.2", 642 | "http-errors": "1.6.2", 643 | "mime": "1.4.1", 644 | "ms": "2.0.0", 645 | "on-finished": "2.3.0", 646 | "range-parser": "1.2.0", 647 | "statuses": "1.3.1" 648 | } 649 | }, 650 | "serve-static": { 651 | "version": "1.13.1", 652 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", 653 | "integrity": "sha512-hSMUZrsPa/I09VYFJwa627JJkNs0NrfL1Uzuup+GqHfToR2KcsXFymXSV90hoyw3M+msjFuQly+YzIH/q0MGlQ==", 654 | "requires": { 655 | "encodeurl": "1.0.1", 656 | "escape-html": "1.0.3", 657 | "parseurl": "1.3.2", 658 | "send": "0.16.1" 659 | } 660 | }, 661 | "setprototypeof": { 662 | "version": "1.1.0", 663 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 664 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 665 | }, 666 | "sntp": { 667 | "version": "2.1.0", 668 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", 669 | "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", 670 | "requires": { 671 | "hoek": "4.2.0" 672 | } 673 | }, 674 | "sshpk": { 675 | "version": "1.13.1", 676 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", 677 | "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", 678 | "requires": { 679 | "asn1": "0.2.3", 680 | "assert-plus": "1.0.0", 681 | "bcrypt-pbkdf": "1.0.1", 682 | "dashdash": "1.14.1", 683 | "ecc-jsbn": "0.1.1", 684 | "getpass": "0.1.7", 685 | "jsbn": "0.1.1", 686 | "tweetnacl": "0.14.5" 687 | } 688 | }, 689 | "statuses": { 690 | "version": "1.3.1", 691 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", 692 | "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=" 693 | }, 694 | "stringstream": { 695 | "version": "0.0.5", 696 | "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", 697 | "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=" 698 | }, 699 | "sync-exec": { 700 | "version": "0.6.2", 701 | "resolved": "https://registry.npmjs.org/sync-exec/-/sync-exec-0.6.2.tgz", 702 | "integrity": "sha1-cX0izFPwzh3vVZQ2LzqJouu5EQU=" 703 | }, 704 | "tough-cookie": { 705 | "version": "2.3.3", 706 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", 707 | "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", 708 | "requires": { 709 | "punycode": "1.4.1" 710 | } 711 | }, 712 | "tunnel-agent": { 713 | "version": "0.6.0", 714 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 715 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 716 | "requires": { 717 | "safe-buffer": "5.1.1" 718 | } 719 | }, 720 | "tweetnacl": { 721 | "version": "0.14.5", 722 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 723 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 724 | "optional": true 725 | }, 726 | "type-is": { 727 | "version": "1.6.15", 728 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", 729 | "integrity": "sha1-yrEPtJCeRByChC6v4a1kbIGARBA=", 730 | "requires": { 731 | "media-typer": "0.3.0", 732 | "mime-types": "2.1.17" 733 | } 734 | }, 735 | "unpipe": { 736 | "version": "1.0.0", 737 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 738 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 739 | }, 740 | "url": { 741 | "version": "0.11.0", 742 | "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", 743 | "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", 744 | "requires": { 745 | "punycode": "1.3.2", 746 | "querystring": "0.2.0" 747 | }, 748 | "dependencies": { 749 | "punycode": { 750 | "version": "1.3.2", 751 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 752 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 753 | } 754 | } 755 | }, 756 | "utils-merge": { 757 | "version": "1.0.1", 758 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 759 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 760 | }, 761 | "uuid": { 762 | "version": "3.1.0", 763 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 764 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 765 | }, 766 | "vary": { 767 | "version": "1.1.2", 768 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 769 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 770 | }, 771 | "verror": { 772 | "version": "1.10.0", 773 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 774 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 775 | "requires": { 776 | "assert-plus": "1.0.0", 777 | "core-util-is": "1.0.2", 778 | "extsprintf": "1.3.0" 779 | } 780 | }, 781 | "wifi-control": { 782 | "version": "2.0.1", 783 | "resolved": "https://registry.npmjs.org/wifi-control/-/wifi-control-2.0.1.tgz", 784 | "integrity": "sha1-WsDVYrSsjXRziAJy4mMezr8ybgk=", 785 | "requires": { 786 | "node-wifiscanner2": "1.2.0", 787 | "sync-exec": "0.6.2" 788 | } 789 | } 790 | } 791 | } 792 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-sonoff-server", 3 | "version": "2.0.0", 4 | "description": "A simple SONOFF Cloud replacement.", 5 | "main": "sonoff.server.module.js", 6 | "dependencies": { 7 | "fs": "^0.0.1-security", 8 | "http": "^0.0.0", 9 | "https": "^1.0.0", 10 | "express": "^4.16.2", 11 | "wifi-control": "^2.0.1", 12 | "nodejs-websocket": "^1.7.1", 13 | "request": "^2.83.0", 14 | "url": "^0.11.0" 15 | }, 16 | "repository": "github:mdopp/simple-sonoff-server", 17 | "devDependencies": {}, 18 | "scripts": { 19 | "test": "echo \"Error: no test specified\" && exit 1" 20 | }, 21 | "keywords": [ 22 | "sonoff" 23 | ], 24 | "author": "Michael Dopp", 25 | "license": "ISC" 26 | } 27 | -------------------------------------------------------------------------------- /sonoff.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "router": { 3 | "SSID": "##########", 4 | "password": "###########" 5 | }, 6 | "server": { 7 | "IP": "192.168.178.31", 8 | "httpPort": 1080, 9 | "httpsPort": 1081, 10 | "websocketPort": 1443 11 | } 12 | } -------------------------------------------------------------------------------- /sonoff.server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const sonoffServer = require("./sonoff.server.module.js"); 4 | var express = require('express'); 5 | var server = express(); 6 | var bodyParser = require('body-parser') 7 | var http = require('http'); 8 | 9 | const config = JSON.parse(fs.readFileSync(path.resolve(__dirname, './sonoff.config.json'))); 10 | 11 | config.logger = { 12 | log: console.log, 13 | warn: console.warn, 14 | error: console.error, 15 | trace: console.info, 16 | debug: console.debug, 17 | }; 18 | 19 | if (process.env.HTTP_PORT !== undefined) 20 | config.server.httpPort = process.env.HTTP_PORT; 21 | if (process.env.HTTPS_PORT !== undefined) 22 | config.server.httpsPort = process.env.HTTPS_PORT; 23 | if (process.env.WEBSOCKET_PORT !== undefined) 24 | config.server.websocketPort = process.env.WEBSOCKET_PORT; 25 | if (process.env.SERVER_IP !== undefined) 26 | config.server.IP = process.env.SERVER_IP; 27 | 28 | 29 | const log = config.logger; 30 | 31 | // call sonoff server for device handling 32 | var devices = sonoffServer.createServer(config); 33 | 34 | // Register body-parser 35 | server.use(bodyParser.json()); 36 | server.use(bodyParser.urlencoded({ extended: true })); 37 | 38 | var httpServer = http.createServer(server) 39 | 40 | httpServer.listen(config.server.httpPort, function () { 41 | log.log('API Server Started On Port %d', config.server.httpPort); 42 | }); 43 | 44 | //returns an simple 0 or 1 for a known device 45 | server.get('/devices/:deviceId/status', function (req, res) { 46 | log.log('GET | %s | %s ', req.method, req.url); 47 | 48 | var d = devices.getDeviceState(req.params.deviceId); 49 | 50 | if (!d || d == "disconnected") { 51 | res.status(404).send('Sonoff device ' + req.params.deviceId + ' not found'); 52 | } else { 53 | res.status(200).send(((d == 'on') ? '1' : '0')); 54 | } 55 | }); 56 | 57 | //switch the device 58 | server.get('/devices/:deviceId/:state', function (req, res) { 59 | log.log('GET | %s | %s ', req.method, req.url); 60 | var d = devices.getDeviceState(req.params.deviceId); 61 | 62 | if (!d || d == "disconnected") { 63 | res.status(404).send('Sonoff device ' + req.params.deviceId + ' not found'); 64 | } else { 65 | switch (req.params.state.toUpperCase()) { 66 | case "1": 67 | case "ON": 68 | res.sendStatus(200); 69 | devices.turnOnDevice(req.params.deviceId); 70 | break; 71 | case "0": 72 | case "OFF": 73 | res.sendStatus(200); 74 | devices.turnOffDevice(req.params.deviceId); 75 | break; 76 | default: 77 | res.status(404).send('Sonoff device ' + req.params.deviceId + ' can not be switched to "' + req.params.state + '", only "ON" and "OFF" are supported currently'); 78 | } 79 | } 80 | }); 81 | 82 | //get the known state of one known device 83 | server.get('/devices/:deviceId', function (req, res) { 84 | log.log('GET | %s | %s ', req.method, req.url); 85 | var d = devices.getDeviceState(req.params.deviceId); 86 | if (!d || d == "disconnected") { 87 | res.status(404).send('Sonoff device ' + req.params.deviceId + ' not found'); 88 | } else { 89 | res.json(devices.getConnectedDevices().find(d => d.id == req.params.deviceId)); 90 | } 91 | }); 92 | 93 | //get a list of known devices 94 | server.get('/devices', function (req, res) { 95 | log.log('GET | %s | %s ', req.method, req.url); 96 | res.json(devices.getConnectedDevices()); 97 | }); 98 | -------------------------------------------------------------------------------- /sonoff.server.module.js: -------------------------------------------------------------------------------- 1 | module.exports.createServer = function (config) { 2 | const CONNECTION_IS_ALIVE_CHECK_INTERVAL = 30000; 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const ws = require("nodejs-websocket"); 7 | const log = config.logger; 8 | 9 | if (config.server.privateKey === undefined) 10 | config.server.privateKey = fs.readFileSync(path.resolve(__dirname, './certs/server.key')); 11 | 12 | if (config.server.certificate === undefined) 13 | config.server.certificate = fs.readFileSync(path.resolve(__dirname, './certs/server.crt')); 14 | 15 | //set initialized parameters 16 | var state = { 17 | knownDevices: [], 18 | listeners: { 19 | onDeviceConnectedListeners: [], 20 | onDeviceDisconnectedListeners: [], 21 | onDeviceUpdatedListeners: [] 22 | } 23 | }; 24 | 25 | // device in der liste finden 26 | state.getDeviceById = (deviceId) => { 27 | return state.knownDevices.find(d => d.id == deviceId); 28 | }; 29 | 30 | state.updateKnownDevice = (device) => { 31 | var updated = false; 32 | 33 | for (var i = 0; i < state.knownDevices.length; i++) { 34 | if (state.knownDevices[i].id == device.id) { 35 | state.knownDevices[i] = device; 36 | updated = true; 37 | callDeviceListeners(state.listeners.onDeviceUpdatedListeners, device); 38 | } 39 | } 40 | if (!updated) { 41 | state.knownDevices.push(device); 42 | callDeviceListeners(state.listeners.onDeviceConnectedListeners, device); 43 | } 44 | }; 45 | 46 | function callDeviceListeners(listeners, device) { 47 | const deviceListeners = listeners[device.id]; 48 | if (!deviceListeners) 49 | return; 50 | deviceListeners.forEach(listener => listener(device.state)); 51 | } 52 | 53 | function addDeviceListener(listeners, deviceId, listener) { 54 | if (!listeners[deviceId]) { 55 | listeners[deviceId] = [listener]; 56 | } else { 57 | listeners[deviceId].push(listener); 58 | } 59 | } 60 | 61 | state.pushMessage = a => { 62 | var rq = { 63 | "apikey": "111111111-1111-1111-1111-111111111111", 64 | "action": a.action, 65 | "deviceid": a.target, 66 | "params": a.value, 67 | "userAgent": "app", 68 | "sequence": Date.now().toString(), 69 | "ts": 0, 70 | "from": "app" 71 | }; 72 | var r = JSON.stringify(rq); 73 | log.trace('REQ | WS | APP | ' + r); 74 | var device = state.getDeviceById(a.target); 75 | if (!device.messages) device.messages = []; 76 | device.messages.push(rq); 77 | device.conn.sendText(r); 78 | }; 79 | 80 | function addConnectionIsAliveCheck(device) { 81 | device.isAlive = true; 82 | 83 | device.isAliveIntervalId = setInterval(() => { 84 | if (device.conn.readyState == device.conn.CONNECTING) return; 85 | if (!device.isAlive) { 86 | clearInterval(device.isAliveIntervalId); 87 | return device.conn.close(408, "connection timed out"); 88 | } 89 | device.isAlive = false; 90 | device.conn.sendPing(); 91 | }, CONNECTION_IS_ALIVE_CHECK_INTERVAL); 92 | 93 | device.conn.on("pong", () => { 94 | device.isAlive = true; 95 | }); 96 | } 97 | 98 | // ----------- api server ------------------------ 99 | // Import libraries 100 | var express = require('express'); 101 | var server = express(); 102 | var bodyParser = require('body-parser') 103 | var https = require('https'); 104 | 105 | // Register body-parser 106 | server.use(bodyParser.json()); 107 | server.use(bodyParser.urlencoded({ extended: true })); 108 | 109 | // Create http(s) server 110 | var credentials = { 111 | key: config.server.privateKey, 112 | cert: config.server.certificate, 113 | rejectUnauthorized: false 114 | }; 115 | 116 | var httpsServer = https.createServer(credentials, server); 117 | 118 | httpsServer.listen(config.server.httpsPort, function () { 119 | log.log('SONOFF Server Started On Port %d', config.server.httpsPort); 120 | }); 121 | 122 | // Register routes 123 | server.post('/dispatch/device', function (req, res) { 124 | log.log('REQ | %s | %s ', req.method, req.url); 125 | log.trace('REQ | %s', JSON.stringify(req.body)); 126 | res.json({ 127 | "error": 0, 128 | "reason": "ok", 129 | "IP": config.server.IP, 130 | "port": config.server.websocketPort 131 | }); 132 | }); 133 | 134 | // Register routes 135 | server.get('/', function (req, res) { 136 | log.log('REQ | %s | %s ', req.method, req.url); 137 | res.send('OK'); 138 | }); 139 | 140 | // ----------- sonoff server ------------------------ 141 | // setup a server, that will respond to the SONOFF requests 142 | // this is the replacement for the SONOFF cloud! 143 | var wsOptions = { 144 | secure: true, 145 | key: config.server.privateKey, 146 | cert: config.server.certificate 147 | }; 148 | 149 | const wsServer = ws.createServer(wsOptions, function (conn) { 150 | log.log("WS | Server is up %s:%s to %s:%s", config.server.IP, config.server.websocketPort, conn.socket.remoteAddress, conn.socket.remotePort); 151 | 152 | conn.on("text", function (str) { 153 | var data = JSON.parse(str); 154 | log.trace('REQ | WS | DEV | %s', JSON.stringify(data)); 155 | res = { 156 | "error": 0, 157 | "deviceid": data.deviceid, 158 | "apikey": "111111111-1111-1111-1111-111111111111" 159 | }; 160 | if (data.action) { 161 | switch (data.action) { 162 | case 'date': 163 | res.date = new Date().toISOString(); 164 | break; 165 | case 'query': 166 | //device wants information 167 | var device = state.getDeviceById(data.deviceid); 168 | if (!device) { 169 | log.error('ERR | WS | Unknown device ', data.deviceid); 170 | } else { 171 | /*if(data.params.includes('timers')){ 172 | log.log('INFO | WS | Device %s asks for timers',device.id); 173 | if(device.timers){ 174 | res.params = [{timers : device.timers}]; 175 | } 176 | }*/ 177 | res.params = {}; 178 | data.params.forEach(p => { 179 | res.params[p] = device[p]; 180 | }); 181 | } 182 | break; 183 | case 'update': 184 | //device wants to update its state 185 | var device = state.getDeviceById(data.deviceid); 186 | if (!device) { 187 | log.error('ERR | WS | Unknown device ', data.deviceid); 188 | } else { 189 | device.state = data.params.switch; 190 | device.conn = conn; 191 | device.rawMessageLastUpdate = data; 192 | device.rawMessageLastUpdate.timestamp = Date.now(); 193 | state.updateKnownDevice(device); 194 | } 195 | 196 | break; 197 | case 'register': 198 | var device = { 199 | id: data.deviceid 200 | }; 201 | 202 | //this is not valid anymore?! type is not based on the first two chars 203 | var type = data.deviceid.substr(0, 2); 204 | if (type == '01') device.kind = 'switch'; 205 | else if (type == '02') device.kind = 'light'; 206 | else if (type == '03') device.kind = 'sensor'; //temperature and humidity. No timers here; 207 | 208 | device.version = data.romVersion; 209 | device.model = data.model; 210 | device.conn = conn; 211 | device.rawMessageRegister = data; 212 | device.rawMessageRegister.timestamp = Date.now(); 213 | addConnectionIsAliveCheck(device); 214 | state.updateKnownDevice(device); 215 | log.log('INFO | WS | Device %s registered', device.id); 216 | break; 217 | default: log.error('TODO | Unknown action "%s"', data.action); break; 218 | } 219 | } else { 220 | if (data.sequence && data.deviceid) { 221 | var device = state.getDeviceById(data.deviceid); 222 | if (!device) { 223 | log.error('ERR | WS | Unknown device ', data.deviceid); 224 | } else { 225 | if (device.messages) { 226 | var message = device.messages.find(item => item.sequence == data.sequence); 227 | if (message) { 228 | device.messages = device.messages.filter(function (item) { 229 | return item !== message; 230 | }) 231 | device.state = message.params.switch; 232 | state.updateKnownDevice(device); 233 | log.trace('INFO | WS | APP | action has been accnowlaged by the device ' + JSON.stringify(data)); 234 | } else { 235 | log.error('ERR | WS | No message send, but received an anser', JSON.stringify(data)); 236 | } 237 | } else { 238 | log.error('ERR | WS | No message send, but received an anser', JSON.stringify(data)); 239 | } 240 | } 241 | } else { 242 | log.error('TODO | WS | Not data action frame\n' + JSON.stringify(data)); 243 | } 244 | } 245 | var r = JSON.stringify(res); 246 | log.trace('RES | WS | DEV | ' + r); 247 | conn.sendText(r); 248 | }); 249 | conn.on("close", function (code, reason) { 250 | log.log("Connection closed: %s (%d)", reason, code); 251 | state.knownDevices.forEach((device, index) => { 252 | if (device.conn != conn) 253 | return; 254 | log.log("Device %s disconnected", device.id); 255 | clearInterval(device.isAliveIntervalId); 256 | callDeviceListeners(state.listeners.onDeviceDisconnectedListeners, device); 257 | device.conn = undefined; 258 | }); 259 | }); 260 | conn.on("error", function (error) { 261 | log.error("Connection error: ", error); 262 | }); 263 | }).listen(config.server.websocketPort); 264 | 265 | return { 266 | //currently all known devices are returned with a hint if they are currently connected 267 | getConnectedDevices: () => { 268 | return state.knownDevices.map(x => { 269 | return { id: x.id, state: x.state, model: x.model, kind: x.kind, version: x.version, isConnected: (typeof x.conn !== 'undefined'), isAlive: x.isAlive, rawMessageRegister: x.rawMessageRegister, rawMessageLastUpdate: x.rawMessageLastUpdate } 270 | }); 271 | }, 272 | 273 | getDeviceState: (deviceId) => { 274 | var d = state.getDeviceById(deviceId); 275 | if (!d || (typeof d.conn == 'undefined')) return "disconnected"; 276 | return d.state; 277 | }, 278 | 279 | turnOnDevice: (deviceId) => { 280 | var d = state.getDeviceById(deviceId); 281 | if (!d || (typeof d.conn == 'undefined')) return "disconnected"; 282 | state.pushMessage({ action: 'update', value: { switch: "on" }, target: deviceId }); 283 | return "on"; 284 | }, 285 | 286 | turnOffDevice: (deviceId) => { 287 | var d = state.getDeviceById(deviceId); 288 | if (!d || (typeof d.conn == 'undefined')) return "disconnected"; 289 | state.pushMessage({ action: 'update', value: { switch: "off" }, target: deviceId }); 290 | return "off"; 291 | }, 292 | 293 | registerOnDeviceConnectedListener: (deviceId, listener) => { 294 | addDeviceListener(state.listeners.onDeviceConnectedListeners, deviceId, listener); 295 | }, 296 | 297 | registerOnDeviceDisconnectedListener: (deviceId, listener) => { 298 | addDeviceListener(state.listeners.onDeviceDisconnectedListeners, deviceId, listener); 299 | }, 300 | 301 | registerOnDeviceUpdatedListener: (deviceId, listener) => { 302 | addDeviceListener(state.listeners.onDeviceUpdatedListeners, deviceId, listener); 303 | }, 304 | 305 | close: () => { 306 | log.log("Stopping server"); 307 | state.knownDevices.forEach(device => device.conn.close()); 308 | httpsServer.close(); 309 | wsServer.close(); 310 | log.log("Stopped server"); 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /sonoff.setupdevice.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const url = require('url'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | var request = require('request'); 6 | const readline = require('readline'); 7 | 8 | var WiFiControl = require('wifi-control'); 9 | 10 | //config parameters for SONOFF 11 | var apSSID = "ITEAD-10"; 12 | var apPWD = "12345678"; 13 | 14 | var parameterMissing = false; 15 | 16 | //config from file 17 | const config = JSON.parse(fs.readFileSync(path.resolve(__dirname, './sonoff.config.json'))); 18 | if (config.server == undefined) config.server = {}; 19 | if (config.router == undefined) config.router = {}; 20 | 21 | //config from environment 22 | if (process.env.SERVER_IP !== undefined) 23 | config.server.IP = process.env.SERVER_IP; 24 | if (process.env.HTTPS_PORT !== undefined) 25 | config.server.httpsPort = process.env.HTTPS_PORT; 26 | if (process.env.WIFI_SSID !== undefined) 27 | config.router.SSID = process.env.WIFI_SSID; 28 | if (process.env.WIFI_PASSWORD !== undefined) 29 | config.router.password = process.env.WIFI_PASSWORD; 30 | 31 | if (config.server.IP == undefined) { 32 | console.error("IP-Adress of your Server running 'sonoff.server.js' or 'sonoff.server.module.js' is missing in the config"); 33 | parameterMissing = true; 34 | } 35 | if (config.server.httpsPort == undefined) { 36 | console.error("Port of your Server is missing in the config"); 37 | parameterMissing = true; 38 | } 39 | if (config.router.SSID == undefined) { 40 | console.error("SSID of the network the SONOFF devices should connect to is missing in the config"); 41 | parameterMissing = true; 42 | } 43 | if (config.router.password == undefined) { 44 | console.error("Password for " + config.router.SSID + " is missing in the config"); 45 | parameterMissing = true; 46 | } 47 | 48 | if (!parameterMissing) { 49 | console.log("using config: ", JSON.stringify(config)); 50 | 51 | console.log("checking if sonoff-server is reachable...", 'https://' + config.server.IP + ':' + config.server.httpsPort + '/'); 52 | request.get({ 53 | url: 'https://' + config.server.IP + ':' + config.server.httpsPort + '/', 54 | rejectUnauthorized: false, 55 | requestCert: true 56 | }, (err, res, data) => { 57 | if (err) { 58 | console.error(`Unable to establish connection to the https sonoff-server: ${err}`); 59 | } else if (res.statusCode !== 200) { 60 | console.error('Unable to connect to the https sonoff-server: ' + res.statusCode); 61 | } else { 62 | // data is already parsed as JSON: 63 | console.log('Connection to the https sonoff-server was successfully established', data); 64 | } 65 | 66 | // Initialize wifi-control package with verbose output 67 | WiFiControl.init({ 68 | debug: true 69 | }); 70 | 71 | //initialize the SONOFF after it has been found 72 | // - set the wlan-network that the sonoff should use 73 | // - set server to which the sonoff should connect (instead of its original cloud) 74 | var _initDevice = () => { 75 | http.get('http://10.10.7.1/device', (res) => { 76 | if (res.statusCode !== 200) { 77 | console.log('Unable to connect to the target device. Code: ' + res.statusCode); 78 | res.resume(); 79 | return; 80 | } 81 | res.setEncoding('utf8'); 82 | var data = ''; 83 | res.on('data', (c) => data += c); 84 | res.on('end', () => { 85 | var response = JSON.parse(data); 86 | var device = { 87 | deviceid: response.deviceid, 88 | apikey: response.apikey 89 | }; 90 | 91 | console.log('device: ' + JSON.stringify(device)); 92 | 93 | //send configuration to device, so that it will use that server as its cloud 94 | request.post('http://10.10.7.1/ap', { 95 | json: true, body: { 96 | "version": 4, 97 | "ssid": config.router.SSID, 98 | "password": config.router.password, 99 | "serverName": config.server.IP, 100 | "port": config.server.httpsPort 101 | } 102 | }, (err, response, body) => { 103 | if (!err && response.statusCode == 200) { 104 | console.log(JSON.stringify(response) + "\t" + body); 105 | } 106 | else { 107 | console.log('Unable to configure endpoint ' + err); 108 | } 109 | }); 110 | }); 111 | }).on('error', (e) => { 112 | console.log(`Unable to establish connection to the device: ${e}`); 113 | }); 114 | }; 115 | 116 | // ---------------------------------------------------------------- 117 | // run ..... 118 | // - scan for SONOFF wlan = ITEAD-100xxxxxx 119 | // - connect to SONOFF 120 | // - setup SONOFF to use local PC as cloud 121 | // ----------------------------------------------------------------- 122 | var find = setInterval(() => { 123 | WiFiControl.scanForWiFi(function (err, response) { 124 | if (err) console.log(err); 125 | var apNet = response.networks.find(n => n.ssid.startsWith(apSSID)); 126 | 127 | if (!apNet) { 128 | console.log('ERR | ' + Date.now() + ' | Sonoff is not in pairing mode. Please, Long press until led start blinking fast.'); 129 | } else { 130 | console.log('OK | Sonoff found in pairing mode.'); 131 | apSSID = apNet.ssid; 132 | clearInterval(find); 133 | apNet.password = apPWD; 134 | WiFiControl.connectToAP(apNet, function (err, response) { 135 | if (err) console.log(err); 136 | console.log('OK | Sonoff paired.', response); 137 | _initDevice(); 138 | }); 139 | } 140 | }); 141 | }, 500); 142 | }); 143 | } 144 | --------------------------------------------------------------------------------