├── examples └── OpenHAB │ ├── transform │ └── boolean.map │ └── items │ └── hs100.items ├── Dockerfile ├── Dockerfile.armhf ├── docker-compose.yaml ├── package.json ├── .gitignore ├── .travis.yml ├── README.md └── index.js /examples/OpenHAB/transform/boolean.map: -------------------------------------------------------------------------------- 1 | true=ON 2 | false=OFF 3 | ON=true 4 | OFF=false 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:slim 2 | 3 | COPY . /node 4 | 5 | RUN cd /node && \ 6 | npm install 7 | 8 | ENTRYPOINT [ "node", "/node/index.js" ] 9 | -------------------------------------------------------------------------------- /Dockerfile.armhf: -------------------------------------------------------------------------------- 1 | FROM arm32v7/node:slim 2 | 3 | COPY . /node 4 | 5 | RUN cd /node && \ 6 | npm install 7 | 8 | ENTRYPOINT [ "node", "/node/index.js" ] 9 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | hs100tomqtt: 5 | image: "dersimn/hs100tomqtt" 6 | volumes: 7 | - ./exampleDeviceTable.json:/node/deviceTable.json:ro 8 | environment: 9 | - HS100TOMQTT_MQTT_URL=mqtt://10.1.1.50 10 | - HS100TOMQTT_DEVICE_TABLE=/node/deviceTable.json 11 | - HS100TOMQTT_VERBOSITY=debug -------------------------------------------------------------------------------- /examples/OpenHAB/items/hs100.items: -------------------------------------------------------------------------------- 1 | Switch HS110_Plug {mqtt=">[mosquitto:hs100/set/ID/poweron:command:*:MAP(boolean.map)], <[mosquitto:hs100/status/ID/poweron:state:MAP(boolean.map)]", autoupdate="false"} 2 | 3 | Number HS110_Plug_Watts "Watt [%f]" {mqtt="<[mosquitto:hs100/status/ID/consumption/power:state:default]"} 4 | Number HS110_Plug_Volts "Volt [%f]" {mqtt="<[mosquitto:hs100/status/ID/consumption/voltage:state:default]"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hs100tomqtt", 3 | "version": "0.0.5", 4 | "description": "In order to use automatic device discovery, you have to run docker with `--net=host` or equivalent configuration.", 5 | "main": "index.js", 6 | "bin": { 7 | "hs100tomqtt": "index.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "Simon Christmann ", 13 | "license": "ISC", 14 | "dependencies": { 15 | "mqtt-smarthome-connect": "latest", 16 | "shortid": "^2.2.14", 17 | "tplink-smarthome-api": "^1.2.0", 18 | "yalm": "^4.1.0", 19 | "yargs": "^11.1.0", 20 | "yetanothertimerlibrary": "^3.1.1" 21 | }, 22 | "devDependencies": {}, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/dersimn/HS100toMQTT.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/dersimn/HS100toMQTT/issues" 29 | }, 30 | "homepage": "https://github.com/dersimn/HS100toMQTT#readme", 31 | "keywords": [ 32 | "mqtt", 33 | "smarthome", 34 | "hs100", 35 | "hs110" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | language: bash 5 | script: 6 | # prepare qemu 7 | - sudo apt-get update 8 | - sudo apt-get install -y wget git 9 | - sudo apt-get remove docker-ce 10 | - echo "deb [arch=amd64 trusted=yes] http://ftp.unicamp.br/pub/linuxpatch/ubuntu/14_04/misc/docker-17.04.0-ce-amd64/ trusty main" | sudo tee /etc/apt/sources.list.d/docker-engine.list 11 | - sudo apt-get update 12 | - sudo apt-get install -y docker-engine 13 | - docker run --rm --privileged multiarch/qemu-user-static:register 14 | - wget https://github.com/multiarch/qemu-user-static/releases/download/v2.9.1/qemu-arm-static.tar.gz -O /tmp/qemu-arm-static.tar.gz 15 | - tar zxvf /tmp/qemu-arm-static.tar.gz -C /tmp 16 | # build image 17 | - docker build --volume type=bind,source=/tmp/qemu-arm-static,target=/usr/bin/qemu-arm-static -t tmptag -f Dockerfile.armhf . 18 | # push image 19 | - > 20 | if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then 21 | docker login -u="$DOCKER_USER" -p="$DOCKER_PASS" 22 | TAG=$(if [ "$TRAVIS_BRANCH" == "master" ]; then echo "armhf"; else echo "$TRAVIS_BRANCH-armhf" ; fi) 23 | docker tag tmptag $DOCKER_USER/hs100tomqtt:$TAG 24 | docker push $DOCKER_USER/hs100tomqtt:$TAG 25 | fi -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NodeJS script to control TP-Link HS100 & HS110 devices via MQTT. 2 | 3 | Topics: 4 | 5 | hs100/maintenance/_bridge /online -> bool 6 | hs100/maintenance//online -> bool 7 | 8 | hs100/status/ -> JSON: {"val":false,"power":0,"voltage":230.68353,"current":0.012407} 9 | hs100/set / <- bool 10 | 11 | (Spaces here are only for formatting, the actual topics won't have them.) 12 | 13 | ## Usage 14 | 15 | ### Native 16 | 17 | git clone HS100toMQTT 18 | cd HS100toMQTT 19 | npm install 20 | node index --help 21 | 22 | ### Docker 23 | 24 | Show all available options: 25 | 26 | docker run --rm dersimn/hs100tomqtt --help 27 | 28 | Start with: 29 | 30 | docker run -d --net=host dersimn/hs100tomqtt -m mqtt:// 31 | 32 | In order to use automatic device discovery, your Docker host has to suport the `--net=host` parameter - not all Docker installations can do this (see *Docker for Mac* [issue](https://forums.docker.com/t/should-docker-run-net-host-work/14215)). 33 | If you prefer to run the script in bridge mode or your host doesn't support host networking, provide a list of IP addresses via the `--devices` option: 34 | 35 | docker run -d dersimn/hs100tomqtt -m mqtt:// --devices="10.1.1.100 10.1.1.101" 36 | 37 | ### Blocking internet access for your devices 38 | 39 | Even though there are currently [no known security issues](https://www.softscheck.com/en/reverse-engineering-tp-link-hs110/) for the HS100 / HS110, if you choose to block internet access for your plugs, be aware that the unterlying [tplink-smarthome-api](https://github.com/plasticrake/tplink-smarthome-api) will throw an error on every polling cycle, because the TP-Link devices will have a wrong time set-up (quite obvious: no Internet, no NTP server, no correct set time and date). 40 | 41 | I've written this [workaround](https://github.com/dersimn/HS100toMQTT/blob/64a364f0336af1cb08791b13346441641fecee26/index.js#L87) until I found a better way to solve this problem: According to [this](https://blog.georgovassilis.com/2016/05/07/controlling-the-tp-link-hs100-wi-fi-smart-plug/) source, the plugs are using `fr.pool.ntp.org` to get their time. If you are able to alter the DNS resolving mechanism of your router (for e.g. when you're using OpenWRT), just make sure to redirect the DNS name to your router IP and setup a local NTP server. 42 | 43 | In OpenWRT you can configure this with: 44 | 45 | `/etc/config/firewall`: 46 | 47 | config rule 48 | option enabled '1' 49 | option src 'lan' 50 | option name 'Block HS110' 51 | option src_mac '00:00:00:00:00:00' 52 | option dest 'wan' 53 | option target 'REJECT' 54 | 55 | `/etc/config/dhcp`: 56 | 57 | config domain 58 | option name 'fr.pool.ntp.org' 59 | option ip '10.1.1.1' 60 | 61 | ## Development 62 | 63 | ### Show debugging output 64 | 65 | For some reason `Ctrl-C` is not working, workaround: 66 | 67 | docker run --rm -it --name=hs100tomqtt dersimn/hs100tomqtt --mqtt-retain=false -m mqtt://MQTT_IP -v debug 68 | Ctrl-P Ctrl-Q 69 | docker stop hs100tomqtt 70 | 71 | ### Manually build 72 | 73 | docker build -t hs100tomqtt . 74 | docker build -t hs100tomqtt:armhf -f Dockerfile.armhf . 75 | 76 | ## Credits 77 | 78 | This project follows [Oliver "owagner" Wagner](https://github.com/owagner)'s architectural proposal for an [mqtt-smarthome](https://github.com/mqtt-smarthome/mqtt-smarthome). 79 | Built by copy-pasting together [Sebastian "hobbyquaker" Raff](https://github.com/hobbyquaker)'s mqtt-smarthome scripts and [Patrick "plasticrake" Seal](https://github.com/plasticrake)'s [tplink-smarthome-api](https://github.com/plasticrake/tplink-smarthome-api). -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const MqttSmarthome = require("mqtt-smarthome-connect"); 4 | const Hs100Api = require('tplink-smarthome-api'); 5 | const log = require('yalm'); 6 | const shortid = require('shortid'); 7 | const Yatl = require('yetanothertimerlibrary'); 8 | 9 | const pkg = require('./package.json'); 10 | const config = require('yargs') 11 | .env('HS100TOMQTT') 12 | .usage(pkg.name + ' ' + pkg.version + '\n' + pkg.description + '\n\nUsage: $0 [options]') 13 | .describe('verbosity', 'possible values: "error", "warn", "info", "debug"') 14 | .describe('name', 'instance name. used as mqtt client id and as prefix for connected topic') 15 | .describe('mqtt-url', 'mqtt broker url. See https://github.com/mqttjs/MQTT.js#connect-using-a-url') 16 | .describe('polling-interval', 'polling interval (in ms) for status updates') 17 | .describe('devices', 'list of device IPs as String, multiple IPs separated by space') 18 | .alias({ 19 | h: 'help', 20 | m: 'mqtt-url', 21 | v: 'verbosity' 22 | }) 23 | .default({ 24 | name: 'hs100', 25 | 'mqtt-url': 'mqtt://127.0.0.1', 26 | 'polling-interval': 3000 27 | }) 28 | .version() 29 | .help('help') 30 | .argv; 31 | 32 | const devices = []; 33 | 34 | log.setLevel(config.verbosity); 35 | log.info(pkg.name + ' ' + pkg.version + ' starting'); 36 | log.debug("loaded config: ", config); 37 | if (typeof config.devices === 'string') { 38 | config.devices.split(" ").forEach( (ip) => { 39 | devices.push({"host":ip, "port":9999}); 40 | }); 41 | } 42 | 43 | const deviceTimer = {}; 44 | 45 | log.info('mqtt trying to connect', config.mqttUrl); 46 | const mqtt = new MqttSmarthome(config.mqttUrl, { 47 | logger: log, 48 | clientId: config.name + '_' + + shortid.generate(), 49 | will: {topic: config.name + '/maintenance/_bridge/online', payload: 'false', retain: true} 50 | }); 51 | mqtt.connect(); 52 | 53 | mqtt.on('connect', () => { 54 | log.info('mqtt connected', config.mqttUrl); 55 | mqtt.publish(config.name + '/maintenance/_bridge/online', true, {retain: true}); 56 | }); 57 | 58 | const client = new Hs100Api.Client({logLevel: config.verbosity, logger: log}); 59 | 60 | client.on('device-new', (device) => { 61 | log.info('hs100 device-new', device.model, device.host, device.deviceId, device.name); 62 | mqtt.publish(config.name + "/maintenance/" + device.deviceId + "/online", true); 63 | mqtt.subscribe(config.name + "/set/" + device.deviceId, (topic, message, packet) => { 64 | if (typeof message === 'object') { 65 | if ('val' in message) { 66 | if (typeof message.val === 'boolean') { 67 | device.setPowerState(message.val); 68 | } 69 | } 70 | } 71 | if (typeof message === 'boolean') { 72 | device.setPowerState(message); 73 | } 74 | deviceTimer[device.deviceId].exec(); 75 | }); 76 | 77 | deviceTimer[device.deviceId] = new Yatl.Timer(() => { 78 | device.getInfo().then(info => { 79 | let message = {}; 80 | message.val = info.sysInfo.relay_state === 1; 81 | message.power = info.emeter.realtime.power; 82 | message.voltage = info.emeter.realtime.voltage; 83 | message.current = info.emeter.realtime.current; 84 | message.energy = info.emeter.realtime.energy; 85 | 86 | mqtt.publish(config.name + "/status/" + device.deviceId, message); 87 | }).catch((err) => { 88 | log.error(err); 89 | }); 90 | }).start(config.pollingInterval); 91 | }); 92 | client.on('device-online', (device) => { 93 | log.debug('hs100 device-online callback', device.name); 94 | mqtt.publish(config.name + "/maintenance/" + device.deviceId + "/online", true); 95 | deviceTimer[device.deviceId].start(config.pollingInterval); 96 | }); 97 | client.on('device-offline', (device) => { 98 | log.warn('hs100 device-offline callback', device.name); 99 | mqtt.publish(config.name + "/maintenance/" + device.deviceId + "/online", false); 100 | deviceTimer[device.deviceId].stop(); 101 | }); 102 | 103 | log.info('Starting Device Discovery'); 104 | client.startDiscovery({ 105 | devices: devices 106 | }); 107 | --------------------------------------------------------------------------------