diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..72a9187 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: node_js +node_js: + - "4" \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..c0bf8e5 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,5 @@ +# Release History + + +* 20160905, V0.9.0 + * Initial Version \ No newline at end of file diff --git a/README.md b/README.md index 001601d..1348716 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ # pimatic-amazing-dash-button -A pimatic plugin for Amazon's dash-buttons + +[![Npm Version](https://badge.fury.io/js/pimatic-amazing-dash-button.svg)](http://badge.fury.io/js/pimatic-amazing-dash-button) +[![Build Status](https://travis-ci.org/mwittig/pimatic-amazing-dash-button.svg?branch=master)](https://travis-ci.org/mwittig/pimatic-amazing-dash-button) +[![Dependency Status](https://david-dm.org/mwittig/pimatic-amazing-dash-button.svg)](https://david-dm.org/mwittig/pimatic-amazing-dash-button) + +A pimatic plugin for Amazon's dash-buttons. It is a pretty light-weight implementation which uses a `ContactSensor` +device abstraction for the dash-button. Auto-discovery of dash-buttons is supported. + +The plugin sniffs for ARP probes which will be sent out by a dash-button when the +button is pressed. The plugin is based on [cap](https://www.npmjs.com/package/cap), a +cross-platform binding for performing packet capturing with node.js. It can be used on *nix and Windows systems. + +## Installation + +**This plugin requires libpcap to capture ARP requests on the network**. On Raspberry PI and comparable systems `libpcap` +must be installed, i.e. `sudo apt-get install libpcap-dev`. +On Windows, [WinPcap](http://www.winpcap.org/install/default.htm) most be installed. + + +## Plugin Configuration + + { + "plugin": "amazing-dash-button", + "interfaceAddress": "192.168.1.15", + } + +The plugin has the following configuration properties: + +| Property | Default | Type | Description | +|:------------------|:---------|:--------|:--------------------------------------------| +| interfaceAddress | - | String | IP address associated with the network interface which shall be used to listen to ARP requests (optional) | + + + +## Device Configuration + +With pimatic v0.9 nad higher dash-button devices can be automatically discovered. + + { + "id": "AmazingDashButton1", + "name": "AmazingDashButton1", + "class": "AmazingDashButton", + "macAddress": "AC:63:BE:B3:BE:78" + } + +The plugin has the following configuration properties: + +| Property | Default | Type | Description | +|:------------------|:---------|:--------|:--------------------------------------------| +| interfaceAddress | - | String | MAC address of the device | +| invert | false | String | If true, invert the contact state, i.e., contact is 'closed' if dash-button not pressed | +| holdTime | 1500 | Integer | The number of milliseconds the contact shall enter the state indicating button pressed (closed if not inverted) | + +## Contributions and Donations + +[![PayPal donate button](https://img.shields.io/paypal/donate.png?color=blue)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=E44SSB34CVXP2) + +Contributions to the project are welcome. You can simply fork the project and create a pull request with +your contribution to start with. If you wish to support my work with a donation I'll highly appreciate this. + +## History + +See [Release History](https://github.com/mwittig/pimatic-amazing-dash-button/blob/master/HISTORY.md). + +## License + +Copyright (c) 2016, Marcus Wittig and contributors. All rights reserved. + +[AGPL-3.0](https://github.com/mwittig/pimatic-amazing-dash-button/blob/master/LICENSE) \ No newline at end of file diff --git a/amazing-dash-button-config-schema.coffee b/amazing-dash-button-config-schema.coffee new file mode 100644 index 0000000..10358ec --- /dev/null +++ b/amazing-dash-button-config-schema.coffee @@ -0,0 +1,16 @@ +module.exports = { + title: "pimatic-amazing-dash-button plugin configuration config options" + type: "object" + properties: + debug: + description: "Debug mode. Writes debug messages to the pimatic log, if set to true." + type: "boolean" + default: false + interfaceAddress: + description: " + the IP address associated with the network interface which shall be used to listen to ARP requests. + If omitted the interface with a bound IP address will be used. + " + type: "string" + required: false +} \ No newline at end of file diff --git a/amazing-dash-button.coffee b/amazing-dash-button.coffee new file mode 100644 index 0000000..ba34e95 --- /dev/null +++ b/amazing-dash-button.coffee @@ -0,0 +1,126 @@ +# The amazing dash-button plugin +module.exports = (env) -> + + Promise = env.require 'bluebird' + cap = require 'cap' + commons = require('pimatic-plugin-commons')(env) + + + # ###AmazingDashButtonPlugin class + class AmazingDashButtonPlugin extends env.plugins.Plugin + + init: (app, @framework, @config) => + @interfaceAddress = @config.interfaceAddress if @config.interfaceAddress? + @invert = @config.invert || false + @_contact = @invert + @debug = @config.debug || false + @base = commons.base @, 'Plugin' + @capture = new cap.Cap() + @buffer = new Buffer(65536) + + process.on "SIGINT", @_stop + @_start() + + # register devices + deviceConfigDef = require("./device-config-schema") + @framework.deviceManager.registerDeviceClass("AmazingDashButton", + configDef: deviceConfigDef.AmazingDashButton, + createCallback: (@config, lastState) => + new AmazingDashButton(@config, @, lastState) + ) + + # auto-discovery + @framework.deviceManager.on('discover', (eventData) => + @framework.deviceManager.discoverMessage 'pimatic-amazing-dash-button', 'Searching for dash-buttons. Press dash-button now!' + @candidatesSeen = [] + @lastId = null + + @arpPacketHandler = (arp) => + candidateArp = arp.info.srcmac.toUpperCase() + strippedArpTrunc = candidateArp.replace(/:/g,'').substring(0,6) + + # List of registered Mac addresses with IEEE as of 18 July 2016 for Amazon Technologies Inc. + # source: https://regauth.standards.ieee.org/standards-ra-web/pub/view.html#registries + amazonVendorIds = [ + "747548", "F0D2F1", "8871E5", "74C246", "F0272D", "0C47C9", + "A002DC", "AC63BE", "44650D", "50F5DA", "84D6D0" + ] + if strippedArpTrunc in amazonVendorIds and candidateArp not in @candidatesSeen + @base.debug 'Amazon device (possibly a dash-button) detected: ' + candidateArp + @candidatesSeen.push candidateArp + @lastId = @base.generateDeviceId @framework, "dash", @lastId + + deviceConfig = + id: @lastId + name: @lastId + class: 'AmazingDashButton' + macAddress: candidateArp + + @framework.deviceManager.discoveredDevice( + 'pimatic-amazing-dash-button', "#{deviceConfig.name} (#{deviceConfig.macAddress})", deviceConfig + ) + + @on 'arpPacket', @arpPacketHandler + @timer = setTimeout( => + @removeListener 'arpPacket', @arpPacketHandler + , eventData.time + ) + ) + + _start: () -> + if @interfaceAddress? + device = cap.findDevice @interfaceAddress + else + device = cap.findDevice() + @base.debug "Sniffing for ARP requests on device", device + + linkType = @capture.open device, 'arp', 10 * 1024 * 1024, @buffer + try + @capture.setMinBytes 0 + catch e + @_base.debug e + + @capture.on "packet", @_rawPacketHandler + + _stop: () => + @capture.removeListener "packet", @_rawPacketHandler + @capture.close(); + + _rawPacketHandler: () => + ret = cap.decoders.Ethernet @buffer + @emit 'arpPacket', ret if ret.info.type is 2054 + + class AmazingDashButton extends env.devices.ContactSensor + # Initialize device by reading entity definition from middleware + constructor: (@config, @plugin, lastState) -> + @id = @config.id + @name = @config.name + @macAddress = @config.macAddress.toUpperCase() + @_invert = @config.invert || false + @_contact = @_invert + @debug = @plugin.debug || false + @base = commons.base @, @config.class + @arpPacketHandler = (arp) => + if arp.info.srcmac.toUpperCase() is @macAddress + @_setContact not @_invert + clearTimeout @timer if @timer? + @timer = setTimeout( => + @_setContact @_invert + @timer = null + , @config.holdTime + ) + super() + @plugin.on 'arpPacket', @arpPacketHandler + + destroy: () -> + clearTimeout @timer if @timer? + @plugin.removeListener 'arpPacket', @arpPacketHandler + super() + + getContact: () -> Promise.resolve @_contact + + + # ###Finally + # Create a instance of my plugin + # and return it to the framework. + return new AmazingDashButtonPlugin \ No newline at end of file diff --git a/device-config-schema.coffee b/device-config-schema.coffee new file mode 100644 index 0000000..3ba1ffb --- /dev/null +++ b/device-config-schema.coffee @@ -0,0 +1,20 @@ +module.exports = { + title: "pimatic-amazing-dash-button device config schemas" + AmazingDashButton: + title: "AmazingDashButton config" + type: "object" + extensions: ["xLink"] + properties: { + macAddress: + description: "MAC address of the dash-button" + type: "string" + invert: + description: "If true, invert the contact state, i.e., contact is 'closed' if dash-button not pressed." + type: "boolean" + default: false + holdTime: + description: "The number of milliseconds the contact shall enter the state indicating button pressed (closed if not inverted)." + type: "integer" + default: 1500 + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..9a70b4a --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "pimatic-amazing-dash-button", + "version": "0.9.0", + "author": { + "name": "Marcus Wittig", + "url": "https://github.com/mwittig" + }, + "main": "amazing-dash-button", + "files": [ + "amazing-dash-button.coffee", + "amazing-dash-button-config-schema.coffee", + "device-config-schema.coffee", + "LICENSE", + "HISTORY.md", + "README.md" + ], + "keywords": [ + "amazon", + "amazon-dash", + "dash", + "button", + "arp", + "pimatic" + ], + "homepage": "https://github.com/mwittig/pimatic-amazing-dash-button/tree/master", + "private": false, + "repository": { + "type": "git", + "url": "git://github.com/mwittig/pimatic-amazing-dash-button.git" + }, + "bugs": { + "url": "https://github.com/mwittig/pimatic-amazing-dash-button/issues" + }, + "license": "AGPL-3.0", + "maintainers": [ + { + "name": "mwittig", + "url": "https://github.com/mwittig" + } + ], + "configSchema": "amazing-dash-button-config-schema.coffee", + "dependencies": { + "cap": "^0.1.1", + "pimatic-plugin-commons": "^0.9.2" + }, + "optionalDependencies": {}, + "peerDependencies": { + "pimatic": ">=0.8.0 <1.0.0" + }, + "engines": { + "node": ">0.8.x", + "npm": ">1.1.x" + } +}