├── .vscode └── launch.json ├── package.json ├── node.mjs ├── .eslintrc.js ├── .gitignore ├── attribute.mjs ├── README.md ├── LICENSE ├── settings.mjs ├── discovery.js ├── app.mjs └── homeeAPI.mjs /.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 | { 9 | "runtimeArgs": ["--experimental-modules"], 10 | "type": "node", 11 | "request": "launch", 12 | "name": "Launch Program", 13 | "program": "${workspaceFolder}/app.mjs" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homeejs", 3 | "version": "0.1.0", 4 | "description": "homee api emulator for use with the homee in homee feature", 5 | "main": "homeeAPI.js", 6 | "dependencies": { 7 | "body-parser": "^1.19.0", 8 | "express": "^4.17.3", 9 | "node-cleanup": "^2.1.2", 10 | "pegjs": "^0.10.0", 11 | "ws": "^7.4.6" 12 | }, 13 | "devDependencies": { 14 | "eslint": "^6.0.1", 15 | "eslint-config-google": "^0.13.0" 16 | }, 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "author": "Tobias Graf", 21 | "license": "ISC" 22 | } 23 | -------------------------------------------------------------------------------- /node.mjs: -------------------------------------------------------------------------------- 1 | export default class Node { 2 | constructor(name, id, profile, attributes) { 3 | this.name = name; 4 | this.id = id; 5 | this.profile = profile; 6 | this.image = 'default'; 7 | this.favorite= 0; 8 | this.order = id; 9 | this.protocol = 1; 10 | this.routing = 1; 11 | this.state = 1; 12 | this.state_changed = 12345; 13 | this.added = 12345; 14 | this.history = 0; 15 | this.cube_type=1; 16 | this.note =''; 17 | this.services= 0; 18 | this.phonetic_name = ''; 19 | this.owner = 0; 20 | this.denied_user_ids = []; 21 | this.attributes = attributes; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'commonjs': true, 4 | 'es6': true, 5 | 'node': true, 6 | }, 7 | 'extends': [ 8 | 'google', 9 | ], 10 | 'globals': { 11 | 'Atomics': 'readonly', 12 | 'SharedArrayBuffer': 'readonly', 13 | }, 14 | 'parserOptions': { 15 | 'ecmaVersion': 2018, 16 | "sourceType": "module" 17 | }, 18 | 'rules': { 19 | "max-len": ["error", 120, 2, { 20 | ignoreUrls: true, 21 | ignoreComments: false, 22 | ignoreRegExpLiterals: true, 23 | ignoreStrings: false, 24 | ignoreTemplateLiterals: false, 25 | }], 26 | "require-jsdoc": ["error", { 27 | "require": { 28 | "FunctionDeclaration": false, 29 | "MethodDefinition": false, 30 | "ClassDeclaration": false, 31 | "ArrowFunctionExpression": false, 32 | "FunctionExpression": false 33 | } 34 | }] 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /attribute.mjs: -------------------------------------------------------------------------------- 1 | export default class Attribute { 2 | constructor( 3 | id, nodeID, instance, min, max, currentValue, targetValue, lastValue, unit, stepValue, editable, type, data) { 4 | this.id = id; 5 | this.node_id = nodeID; 6 | this.instance = instance; 7 | this.minimum = min; 8 | this.maximum = max; 9 | this.current_value = currentValue; 10 | this.target_value = targetValue; 11 | this.last_value = lastValue; 12 | this.unit = unit; 13 | this.step_value = stepValue; 14 | this.editable = editable; 15 | this.type = type; 16 | this.state = 1; 17 | this.last_changed = 12345555; 18 | this.changed_by = 1; 19 | this.changed_by_id = 0; 20 | this.based_on = 1; 21 | this.data = data; 22 | } 23 | 24 | setTargetValue(targetValue) { 25 | if ( targetValue === true) { 26 | targetValue = 1; 27 | } else if ( targetValue === false) { 28 | targetValue = 0; 29 | } 30 | this.target_value = parseFloat(targetValue); 31 | } 32 | setCurrentValue(currentValue) { 33 | if ( currentValue === true) { 34 | currentValue = 1; 35 | } else if ( currentValue === false) { 36 | currentValue = 0; 37 | } 38 | this.current_value = parseFloat(currentValue); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homeejs 2 | This mocks the homee api in a way that it can be used with the "homee in homee" feature. This makes it possible to add all kinds of devices to homee. 3 | 4 | # WIP 5 | 6 | See app.mjs for basic example how to use this 7 | 8 | You need to use `--experimental-modules` and node v10.xx to run this. (I used 10.16.3). 9 | 10 | # Usage 11 | 12 | To connect to homeejs using the homee app, just use the ip or the `name` (dafault is `HOMEEJS`) you used when creating the api `new HomeeAPI('HOMEEJS');` see [app.mjs](https://github.com/tobiasgraf/homeejs/blob/master/app.mjs#L23). 13 | Use a all UPPERCASE NAME, homee has some trouble when with lower case names when searching for new devices. 14 | 15 | There is no user management atm, just use username "homee" and any password. 16 | 17 | # Node-Red 18 | 19 | A Node-Red Plugin has been developed out of this and can be found [here](https://github.com/stfnhmplr/node-red-contrib-homee). 20 | There is also a (german) [Blogpost](https://himpler.com/blog/virtuelle-geraete-in-homee-mit-node-red) about the Node-Red plugin and a [homee community thread](https://community.hom.ee/t/mal-wieder-virtuelle-geraete-jetzt-aber-richtig/24831) 21 | 22 | # Disclaimer 23 | This is a purely private project and has no connection with Codeatelier GmbH. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /settings.mjs: -------------------------------------------------------------------------------- 1 | export default class settings { 2 | constructor(homeeID) { 3 | this.address = ''; 4 | this.city = ''; 5 | this.zip = 11111; 6 | this.state = 'BW'; 7 | this.latitude = ''; 8 | this.longitude = ''; 9 | this.country = 'Germany'; 10 | this.language = 'de'; 11 | this.wlan_dhcp = 1; 12 | this.remote_access =1; 13 | this.beta= 0; 14 | this.webhooks_key ='WEBHOOKKEY'; 15 | this.automatic_location_detection = 0; 16 | this.polling_interval = 60; 17 | this.timezone = 'Europe%2FBerlin'; 18 | this.enable_analytics = 0; 19 | this.wlan_enabled=1; 20 | this.wlan_ip_address = '192.168.178.222'; 21 | this.wlan_ssid = 'homeeWifi'; 22 | this.wlan_mode = 2; 23 | this.online =0; 24 | this.lan_enabled=1; 25 | this.available_ssids = ['homeeWifi']; 26 | this.time = 1562707105; 27 | this.civil_time = '2019-07-09 23:18:25'; 28 | this.version = '2.25.0 (ed9c50)'; 29 | this.uid = homeeID; 30 | this.gateway_id = 1313337; 31 | this.cubes = []; 32 | this.extensions= { 33 | 'weather': { 34 | 'enabled': 1, 35 | }, 36 | 'amazon_alexa': { 37 | 'enabled': 0, 38 | }, 39 | 'google_assistant': { 40 | 'enabled': 0, 41 | 'syncing': 0, 42 | }, 43 | 'apple_homekit': { 44 | 'enabled': 0, 45 | 'paired': 0, 46 | 'config_number': 1, 47 | 'syncing': 0, 48 | }, 49 | 'ftp': { 50 | 'enabled': 0, 51 | }, 52 | 'history': { 53 | 'enabled': 0, 54 | }, 55 | 'backup': { 56 | 'enabled': 0, 57 | }, 58 | }; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /discovery.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | start: start, 3 | }; 4 | 5 | /** 6 | * starts the discovery server and listens for given alias 7 | * @param {stirng} alias 8 | */ 9 | function start(alias) { 10 | // Require dgram module. 11 | const dgram = require('dgram'); 12 | 13 | // Create udp server socket object. 14 | const server = dgram.createSocket('udp4'); 15 | 16 | // Make udp server listen on port 15263. 17 | server.bind(15263); 18 | 19 | server.on('error', function(err) { 20 | console.log('Failed to start Discovery Server - continuing without Discovery Server. Use IP instead! ' + err); 21 | return; 22 | }); 23 | 24 | /** 25 | * compares two string, case insensitive 26 | * @param {*} a 27 | * @param {*} b 28 | * @return {bool} true if equal, false otherwise 29 | */ 30 | function iequals(a, b) { 31 | return typeof a === 'string' && typeof b === 'string' 32 | ? a.localeCompare(b, undefined, {sensitivity: 'accent'}) === 0 33 | : a === b; 34 | } 35 | 36 | // When udp server receive message. 37 | server.on('message', function(message, rinfo) { 38 | // Create output message. 39 | const output = 'Discovery server receive message : ' + message + '\n'; 40 | // Print received message in stdout, here is log console. 41 | if ( iequals(message.toString(), alias.toString())) { 42 | // initialized:00055107F7B1:00055107F7B1:homee 43 | // eslint-disable-next-line new-cap 44 | message = new Buffer.from('initialized:' + alias + ':' + alias +':homee'); 45 | 46 | // Send message to udp server through client socket. 47 | server.send(message, 0, message.length, rinfo.port, rinfo.address); 48 | } else { 49 | console.log('message did not match alias: ' + message + '__vs__' + alias); 50 | } 51 | process.stdout.write(output); 52 | }); 53 | 54 | // When udp server started and listening. 55 | server.on('listening', function() { 56 | // Get and print udp server listening ip address 57 | // and port number in log console. 58 | const address = server.address(); 59 | console.log('UDP Server started and listening on ' + address.address + ':' + address.port); 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /app.mjs: -------------------------------------------------------------------------------- 1 | import HomeeAPI from './homeeAPI.mjs'; 2 | import Node from './node.mjs'; 3 | import Attribute from './attribute.mjs'; 4 | 5 | const nodes = [ 6 | new Node('Light LivingRoom', 10, 15, [ // nodeID = 10, Profile = 15 = Dimmer 7 | new Attribute(10, 10, 0, 0, 1, 0, 0, 0, '', 1, 1, 1, ''), // on off 8 | new Attribute(11, 10, 0, 0, 100, 0, 0, 0, '%25', 1, 1, 2, ''), // dimming level 9 | ]), 10 | new Node('Shutter LivingRoom', 20, 2004, [ // nodeID = 20, Profile = 2004 = Roller shutter 11 | new Attribute(20, 20, 0, 0, 4, 0, 0, 0, '', 1, 1, 135, ''), // updown 12 | new Attribute(21, 20, 0, 0, 100, 0, 0, 0, '%25', 1, 1, 15, ''), // position 13 | ]), 14 | new Node('balcony door LivingRoom', 30, 2000, [ // nodeID = 30, Profile = 2000 = Door/Window Sensor 15 | new Attribute(30, 30, 0, 0, 1, 0, 0, 0, '', 1, 0, 14, ''), // open close 16 | ]), 17 | new Node('temperature LivingRoom', 40, 3009, [ // nodeID = 40, Profile = 3009 = Temperature Sensor 18 | new Attribute(40, 40, 0, 0, 40, 0, 0, 0, '%C2%B0C', 0.1, 0, 5, ''), // temperature sensor 19 | ]), 20 | new Node('Philips Hue Bewegungsmelder', 50, 4035, [ // nodeID = 50, Profile = 4035 = CANodeProfilePresenceDetectorWithTemperatureAndBrightnessSensor 21 | new Attribute(50, 50, 0, 0, 40, 0, 0, 0, '%C2%B0C', 1, 0, 5, ''), // temperature sensor 22 | new Attribute(51, 50, 0, 0, 1, 0, 0, 0, '', 1, 0, 76, ''), // presence alarm 23 | new Attribute(52, 50, 0, 0, 50000, 0, 0, 0, 'lx', 1, 0, 76, ''), // brightness alarm 24 | ]), 25 | new Node('Danfoss Z Thermostat', 60, 3006, [ // nodeID = 60, Profile = 3006 = RadiatorThermostat 26 | new Attribute(60, 60, 0, 0, 40, 0, 0, 0, '%C2%B0C', 1, 1, 6, ''), // target temperature 27 | ]), 28 | 29 | ]; 30 | 31 | const api = new HomeeAPI('HOMEEJS'); 32 | api.setNodes(nodes); 33 | 34 | // Get Nodes is handled by the api itself, but if you want to do something else here ... 35 | // api.on('GET:nodes', (wantedNodes) => { 36 | // if ( wantedNodes.length == 0 ) { 37 | // console.log('user wants all nodes'); 38 | // } else { 39 | // for (const wantedNode of wantedNodes) { 40 | // console.log('user wants node ' + wantedNode); 41 | // } 42 | // } 43 | // }); 44 | // api.on('GET:all', () => { 45 | // console.log('user wants Get all '); 46 | // }); 47 | 48 | api.on('PUT:attributes', (attributeID, nodeID, targetValue, parsed) => { 49 | console.log('user wants to switch attribute %i of Node %i to %f ', attributeID, nodeID, targetValue); 50 | }); 51 | 52 | // api.on('get', (parsed) => { 53 | // console.log('GET Event: %o', parsed); 54 | // }); 55 | // api.on('post', (parsed) => { 56 | // console.log('POST Event: %o', parsed); 57 | // }); 58 | // api.on('put', (parsed) => { 59 | // console.log('PUT Event: %o', parsed); 60 | // }); 61 | 62 | api.start(); 63 | 64 | function findAttribute(attributeID) { 65 | let found; 66 | nodes.find((node) => node.attributes.find((attr) => attr.id === attributeID && (found = attr))); 67 | return found; 68 | } 69 | 70 | function valueChanged(oldValue, newValue) { 71 | const Attr = findAttribute(10); 72 | console.log('changed from %o to %o', oldValue, newValue); 73 | Attr.setCurrentValue(newValue); 74 | Attr.setTargetValue(newValue); 75 | api.send(JSON.stringify({'attribute': Attr})); 76 | } 77 | -------------------------------------------------------------------------------- /homeeAPI.mjs: -------------------------------------------------------------------------------- 1 | // create EventEmitter object 2 | import EventEmitter from 'events'; 3 | 4 | import WebSocket from 'ws'; 5 | import http from 'http'; 6 | import url from 'url'; 7 | import express from 'express'; 8 | import bodyParser from 'body-parser'; 9 | import discovery from './discovery.js'; 10 | import Settings from './settings.mjs'; 11 | 12 | /** 13 | * class that handles the homee api 14 | */ 15 | export default class HomeeAPI extends EventEmitter { 16 | constructor(homeeID) { 17 | super(); 18 | this.homeeID = homeeID; 19 | this.wss = new WebSocket.Server({noServer: true}); 20 | this.app = express(); 21 | this.app.use(bodyParser.json()); 22 | this.app.use(bodyParser.urlencoded({extended: false})); 23 | this.server = http.createServer(this.app); 24 | this.AccessToken = 'SUPERSECUREACCESSTOKENTHISNEEDSTOBECHANGEDATSOMEPOINT'; 25 | } 26 | /** test events */ 27 | testEvent() { 28 | this.emit('test', 'some test data'); 29 | } 30 | 31 | send(message) { 32 | this.wss.clients.forEach( function each(client) { 33 | if (client.readyState === WebSocket.OPEN) { 34 | client.send(message); 35 | } 36 | }); 37 | } 38 | start() { 39 | discovery.start(this.homeeID); 40 | 41 | this.server.listen(7681, '0.0.0.0'); 42 | this.server.on('error', function(err) { 43 | console.log('homee Api Server caused an error: ' + err); 44 | console.log('cannot run without api server, exiting'); 45 | process.exit(1); 46 | }); 47 | 48 | this.server.on('upgrade', this.OnHttpServerUpgrade.bind(this) ); 49 | this.wss.on('connection', (ws) => { 50 | this.ws = ws; 51 | ws.on('message', (message) => { 52 | this.onWSMessage( message, ws); 53 | } ); 54 | }); 55 | this.app.options('/access_token', this.OnExpressOptionAccessToken.bind(this) ); 56 | this.app.post('/access_token', this.OnExpressPostAccessToken.bind(this) ); 57 | } 58 | 59 | setNodes(nodes) { 60 | this.nodes = nodes; 61 | } 62 | 63 | onWSMessage( message, ws) { 64 | if ( message !== 'ping' ) { 65 | console.log('received: %s', message); 66 | } 67 | const parsed = parse(message); 68 | // console.log('parsed data: %o', parsed); 69 | this.emit(parsed.method, parsed); 70 | if ( parsed.method === 'ping') { 71 | ws.send('pong'); 72 | } 73 | if ( iequals(parsed.method, 'get') ) { 74 | if (iequals( parsed.target, 'nodes')) { 75 | if ( parsed.commands['nodes'] == 0 ) { 76 | this.emit('GET:nodes', []); 77 | ws.send(JSON.stringify({'nodes': this.nodes})); 78 | } else { 79 | this.emit('GET:nodes', parsed.commands['nodes']); 80 | } 81 | } 82 | if ( parsed.target === 'all') { 83 | this.emit('GET:all', []); 84 | ws.send(JSON.stringify( {'all': 85 | { 86 | 'nodes': this.nodes, 87 | 'users': [ 88 | { 89 | 'id': 1, 90 | 'username': 'homee', 91 | 'forename': 'homee', 92 | 'surname': 'homee', 93 | 'image': '', 94 | 'role': 2, 95 | 'type': 1, 96 | 'email': '', 97 | 'phone': '', 98 | 'added': '27. Jan 2016 13:37:00 (1453898220)', 99 | 'homee_name': '🏠', 100 | 'homee_image': 'profileicon_5_1', 101 | 'access': 1, 102 | 'cube_push_notifications': 1, 103 | 'cube_email_notifications': 0, 104 | 'cube_sms_notifications': 0, 105 | 'node_push_notifications': 1, 106 | 'node_email_notifications': 0, 107 | 'node_sms_notifications': 0, 108 | 'warning_push_notifications': 1, 109 | 'warning_email_notifications': 0, 110 | 'warning_sms_notifications': 0, 111 | 'update_push_notifications': 1, 112 | 'update_email_notifications': 0, 113 | 'update_sms_notifications': 0, 114 | 'api_push_notifications': 0, 115 | 'api_email_notifications': 0, 116 | 'api_sms_notifications': 0}], 117 | 'groups': [], 118 | 'relationships': [], 119 | 'homeegrams': [], 120 | 'settings': new Settings(this.homeeID), 121 | 'plans': [], 122 | }, 123 | } )); 124 | } 125 | if ( parsed.target === 'settings') { 126 | this.emit('GET:settings', []); 127 | ws.send(JSON.stringify( {'settings': 128 | new Settings(this.homeeID), 129 | } )); 130 | } 131 | } 132 | if ( parsed.method === 'put' ) { 133 | if ( parsed.target === 'attributes') { 134 | if ( parsed.commands['attributes'] === 0) { 135 | console.log('ids sind %o', parsed.parameters['ids']); 136 | parsed.parameters['ids'].split(',').forEach((id) => { 137 | console.log('id %o', id); 138 | if (id !== '') { 139 | this.emit('PUT:attributes', 140 | id, 141 | parsed.commands['nodes'], 142 | parsed.parameters['target_value'], 143 | parsed); 144 | } 145 | }); 146 | } else { 147 | this.emit('PUT:attributes', 148 | parsed.commands['attributes'], 149 | parsed.commands['nodes'], 150 | parsed.parameters['target_value'], 151 | parsed); 152 | } 153 | } 154 | } 155 | if ( parsed.method === 'post' ) { 156 | if ( parsed.target === 'nodes') { 157 | const protocol = parsed.parameters['protocol']; 158 | const check = parsed.parameters['compatibility_check']; 159 | const version = parsed.parameters['my_version']; 160 | const startPairing = parsed.parameters['start_pairing']; 161 | if ( protocol === '21') { 162 | if ( check === '1') { 163 | // just echo back what homee wants to hear 164 | ws.send('{' + 165 | ' "compatibility_check":{' + 166 | ' "compatible": true,' + 167 | ' "account": true,' + 168 | ' "external_homee_status": "none",' + 169 | ' "your_version": true,' + 170 | ' "my_version": "' + version + '" ,' + 171 | ' "my_homeeID": "' +this.homeeID + '"' + 172 | ' }' + 173 | '}'); 174 | } 175 | if ( startPairing === '1') { 176 | // just echo back what homee wants to hear 177 | ws.send('{' + 178 | ' "pairing":{' + 179 | ' "access_token": "' + this.AccessToken + '",' + 180 | ' "expires": 31536000,' + 181 | ' "userID": 1,' + 182 | ' "deviceID": 1' + 183 | ' }' + 184 | '}'); 185 | } 186 | } 187 | } 188 | } 189 | if ( parsed.method === 'delete' ) { 190 | if ( parsed.target === 'devices') { 191 | ws.send('{\n' + 192 | ' "warning": {\n' + 193 | ' "code": 600,\n' + 194 | ' "description": "Your device got removed.",\n' + 195 | ' "message": "You have been logged out.",\n' + 196 | ' "data": {}\n' + 197 | ' }\n' + 198 | '}'); 199 | ws.close(4444, 'DEVICE_DISCONNECT'); 200 | } 201 | } 202 | } 203 | 204 | OnHttpServerUpgrade(request, socket, head) { 205 | const ParsedUrl = url.parse(request.url); 206 | const pathname = ParsedUrl.pathname; 207 | 208 | if (pathname === '/connection') { 209 | const params = new URLSearchParams(ParsedUrl.query); 210 | const GivenAccessToken = params.get('access_token'); 211 | if (GivenAccessToken == this.AccessToken) { 212 | this.wss.handleUpgrade(request, socket, head, (ws) => { 213 | this.wss.emit('connection', ws, request); 214 | }); 215 | } else { 216 | socket.destroy(); 217 | } 218 | } else { 219 | socket.destroy(); 220 | } 221 | } 222 | 223 | OnExpressOptionAccessToken(req, res) { 224 | const header = { 225 | 'Access-Control-Allow-Headers': 'Authorization', 226 | 'Access-Control-Allow-Methods': 'POST, DELETE', 227 | 'Access-Control-Allow-Origin': '*', 228 | 'Content-Length': '0', 229 | 'Content-Type': 'application/x-www-form-urlencoded', 230 | 'Server': 'homeejs Server', 231 | }; 232 | res.writeHead(200, header); 233 | res.end(); 234 | } 235 | 236 | OnExpressPostAccessToken(req, res) { 237 | // console.log('body: %o , header %o', req.body, req.headers); 238 | const auth = req.headers['authorization']; // auth is in base64(username:password) so we need to decode the base64 239 | console.log('Authorization Header is: ', auth); 240 | 241 | if (auth) { // The Authorization was passed in so now we validate it 242 | const tmp = auth.split(' '); // Split on a space, "Basic Y2hhcmxlczoxMjM0NQ==" 243 | 244 | // eslint-disable-next-line new-cap 245 | const buf = new Buffer.from(tmp[1], 'base64'); // create a buffer and tell it the data coming in is base64 246 | const PlainAuth = buf.toString(); // read it back out as a string 247 | 248 | console.log('Decoded Authorization ', PlainAuth); 249 | 250 | // At this point plain_auth = "username:password" 251 | 252 | const creds = PlainAuth.split(':'); // split on a ':' 253 | const username = creds[0]; 254 | const password = creds[1]; 255 | 256 | // eslint-disable-next-line max-len 257 | // TODO: handle username / password 258 | if ( username === 'homee' ) { // && password = sha512('12345678') 259 | // eslint-disable-next-line max-len 260 | const response = 'access_token=' + this.AccessToken + '&user_id=1&device_id=1&expires=31536000'; 261 | res.writeHead(200, { 262 | 'Content-Type': 'application/x-www-form-urlencoded', 263 | 'Access-Control-Allow-Origin': '*', 264 | 'Server': 'homeejs Server'}); 265 | res.write(response); 266 | res.end(); 267 | } else { 268 | res.statusCode = 401; // Force them to retry authentication 269 | res.writeHead(401, { 270 | 'WWW-Authenticate': 'Basic realm="Secure Area"', 271 | 'Content-Type': 'application/x-www-form-urlencoded', 272 | 'Access-Control-Allow-Origin': '*', 273 | 'Server': 'homeejs Server'}); 274 | const response = ' {"errors":[{"status":"401","detail":"Invalid username or password.","blockTime": 2 }]}'; 275 | res.write(response); 276 | res.end(); 277 | } 278 | } 279 | } 280 | }; 281 | 282 | /** 283 | * compares two string, case insensitive 284 | * @param {*} a 285 | * @param {*} b 286 | * @return {bool} true if equal, false otherwise 287 | */ 288 | function iequals(a, b) { 289 | return typeof a === 'string' && typeof b === 'string' 290 | ? a.localeCompare(b, undefined, {sensitivity: 'accent'}) === 0 291 | : a === b; 292 | } 293 | 294 | function parse(message) { 295 | // "put:nodes/1/attributes/1?target_value=1" 296 | if ( message.indexOf(':') === -1 ) { 297 | // special commands 298 | if ( iequals(message, 'ping')) { 299 | return {method: 'ping', commands: '', target: '', parameters: {}}; 300 | } 301 | } 302 | let [method, a] = message.split(':'); 303 | method = method.toLowerCase(); 304 | const [commands, parameters] = a.split('?'); 305 | let c; 306 | const cmds = {}; 307 | let targetCmd; 308 | for ( const t of commands.split('/')) { 309 | if ( isNaN(t) ) { 310 | c = t.toLowerCase(); 311 | continue; 312 | } 313 | cmds[c] = t; 314 | targetCmd = c.toLowerCase(); 315 | c = ''; 316 | } 317 | if ( c !== '') { 318 | targetCmd = c; 319 | cmds[c.toLowerCase()] = 0; 320 | } 321 | if ( !targetCmd ) { 322 | // in case there are no parameter or anything (like get:nodes) 323 | targetCmd = commands.toLowerCase(); 324 | cmds[commands.toLowerCase()] = 0; 325 | } 326 | const searchParams = new URLSearchParams(parameters); 327 | 328 | const paras = {}; 329 | searchParams.forEach(function(value, key) { 330 | console.log(value, key); 331 | paras[key.toLowerCase()] = value; 332 | }); 333 | 334 | return {method: method, commands: cmds, target: targetCmd, parameters: paras}; 335 | } 336 | --------------------------------------------------------------------------------