Skip to content

Commit c204cd9

Browse files
committed
added support for DHCP-based detection of dash buttons, issue #6
updated dependencies
1 parent 3cf2d11 commit c204cd9

File tree

3 files changed

+110
-35
lines changed

3 files changed

+110
-35
lines changed

amazing-dash-button.coffee

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = (env) ->
44
Promise = env.require 'bluebird'
55
net = require 'net'
66
cap = require 'cap'
7+
bootp = require './bootp'
78
commons = require('pimatic-plugin-commons')(env)
89

910

@@ -41,32 +42,32 @@ module.exports = (env) ->
4142
@candidatesSeen = []
4243
@lastId = null
4344

44-
@arpPacketHandler = (arp) =>
45-
candidateArpAddress = arp.info.sendermac
46-
if candidateArpAddress not in @candidatesSeen
47-
@base.debug 'Amazon device detected: ' + candidateArpAddress
48-
@candidatesSeen.push candidateArpAddress
49-
@_probeChromeCastPort(arp.info.senderip).then (probeSucceeded) =>
45+
@candidateInfoHandler = (info) =>
46+
candidateAddress = info.mac
47+
if candidateAddress not in @candidatesSeen
48+
@base.debug 'Amazon device detected: ' + candidateAddress
49+
@candidatesSeen.push candidateAddress
50+
@_probeChromeCastPort(info.ip).then (probeSucceeded) =>
5051
if probeSucceeded
51-
@base.debug 'Amazon device appears to be a Chromecast server: ' + candidateArpAddress
52+
@base.debug 'Amazon device appears to be a Chromecast server: ' + candidateAddress
5253
else
5354
@lastId = @base.generateDeviceId @framework, "dash", @lastId
5455

5556
deviceConfig =
5657
id: @lastId
5758
name: @lastId
5859
class: 'AmazingDashButton'
59-
macAddress: candidateArpAddress
60+
macAddress: candidateAddress
6061

6162
@framework.deviceManager.discoveredDevice(
6263
'pimatic-amazing-dash-button',
63-
"#{deviceConfig.name} (#{deviceConfig.macAddress}, #{arp.info.senderip})",
64+
"#{deviceConfig.name} (#{deviceConfig.macAddress}, #{info.ip})",
6465
deviceConfig
6566
)
6667

67-
@on 'arpPacket', @arpPacketHandler
68+
@on 'candidateInfo', @candidateInfoHandler
6869
@timer = setTimeout( =>
69-
@removeListener 'arpPacket', @arpPacketHandler
70+
@removeListener 'candidateInfo', @candidateInfoHandler
7071
, eventData.time
7172
)
7273
)
@@ -98,7 +99,8 @@ module.exports = (env) ->
9899
left + " or " + right
99100
)
100101

101-
linkType = @capture.open device, "arp and (#{filter})", 10 * 65536, @buffer
102+
pcapFilter = "(arp or (udp and src port 68 and dst port 67 and udp[247:4] == 0x63350103)) and (#{filter})"
103+
linkType = @capture.open device, pcapFilter, 10 * 65536, @buffer
102104
try
103105
@capture.setMinBytes 0
104106
catch e
@@ -112,27 +114,42 @@ module.exports = (env) ->
112114

113115
_rawPacketHandler: () =>
114116
ret = cap.decoders.Ethernet @buffer
115-
if ret.info.type is cap.decoders.PROTOCOL.ETHERNET.ARP
116-
@emit 'arpPacket', cap.decoders.ARP @buffer, ret.offset
117+
candidateInfo = {}
118+
if ret.info.type is cap.decoders.PROTOCOL.ETHERNET.IPV4
119+
r = cap.decoders.IPV4 @buffer, ret.offset
120+
dhcp = bootp.BOOTP @buffer, r.offset + 8
121+
candidateInfo.mac = dhcp.info.clientmac
122+
candidateInfo.ip = dhcp.info.requestedip
123+
@base.debug "DHCP", candidateInfo
124+
@emit 'candidateInfo', candidateInfo
125+
else if ret.info.type is cap.decoders.PROTOCOL.ETHERNET.ARP
126+
arp = cap.decoders.ARP @buffer, ret.offset
127+
candidateInfo.mac = arp.info.sendermac
128+
candidateInfo.ip = arp.info.senderip
129+
@base.debug "ARP", candidateInfo
130+
@emit 'candidateInfo', candidateInfo
117131

118132
_probeChromeCastPort: (host, port=8008) ->
119-
client = new net.Socket
120-
return new Promise( (resolve) =>
121-
client.setTimeout 3000, =>
122-
@base.debug "Timeout"
123-
resolve false
124-
client.on "error", (error) =>
125-
@base.debug error
133+
if host?.length
134+
client = new net.Socket
135+
new Promise( (resolve) =>
136+
client.setTimeout 3000, =>
137+
@base.debug "Timeout"
138+
resolve false
139+
client.on "error", (error) =>
140+
@base.debug error
141+
resolve false
142+
client.connect port, host, =>
143+
@base.debug "Connected to device #{host}:#{port}"
144+
resolve true
145+
)
146+
.catch =>
147+
@base.debug "Exception"
126148
resolve false
127-
client.connect port, host, =>
128-
@base.debug "Connected to device #{host}:#{port}"
129-
resolve true
130-
)
131-
.catch =>
132-
@base.debug "Exception"
133-
resove false
134-
.finally =>
135-
client.destroy()
149+
.finally =>
150+
client.destroy()
151+
else
152+
Promise.resolve false
136153

137154

138155
class AmazingDashButton extends env.devices.ContactSensor
@@ -153,8 +170,9 @@ module.exports = (env) ->
153170
@_contact = @_invert
154171
@debug = @plugin.debug || false
155172
@base = commons.base @, @config.class
156-
@arpPacketHandler = (arp) =>
157-
if arp.info.sendermac is @macAddress
173+
@candidateInfoHandler = (info) =>
174+
if not @timer? and info.mac is @macAddress
175+
@base.debug "Amazon dash button triggered (#{info.mac})"
158176
@_setContact not @_invert
159177
clearTimeout @timer if @timer?
160178
@timer = setTimeout( =>
@@ -163,11 +181,11 @@ module.exports = (env) ->
163181
, @config.holdTime
164182
)
165183
super()
166-
@plugin.on 'arpPacket', @arpPacketHandler
184+
@plugin.on 'candidateInfo', @candidateInfoHandler
167185

168186
destroy: () ->
169187
clearTimeout @timer if @timer?
170-
@plugin.removeListener 'arpPacket', @arpPacketHandler
188+
@plugin.removeListener 'candidateInfo', @candidateInfoHandler
171189
super()
172190

173191
getContact: () -> Promise.resolve @_contact

bootp.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// a fragmentary decoder for BOOTP to gather the requested IP address (DHCP option 50)
2+
exports.BOOTP = function(b, offset) {
3+
offset || (offset = 0);
4+
var i;
5+
var ret = {
6+
info: {
7+
clientmac: '',
8+
requestedip: ''
9+
},
10+
offset: undefined
11+
};
12+
13+
ret.info.messagetype = b.readInt8(offset++, true);
14+
ret.info.hardwaretype = b.readInt8(offset++, true);
15+
ret.info.hardwareaddrlen = b.readInt8(offset++, true);
16+
ret.info.hops = b.readInt8(offset++, true);
17+
ret.info.transActionId = b.readUInt32BE(offset, true);
18+
// skip timer and address fields
19+
offset += 24;
20+
21+
// 32-bit Destination MAC Address
22+
for (i = 0; i < 6; ++i) {
23+
if (b[offset] < 16)
24+
ret.info.clientmac += '0';
25+
ret.info.clientmac += b[offset++].toString(16);
26+
if (i < 5)
27+
ret.info.clientmac += ':';
28+
}
29+
//offset += 6
30+
// address padding
31+
offset+= 10;
32+
// server name
33+
offset+= 64;
34+
// boot filename
35+
offset+= 128;
36+
ret.info.cookie = b.toString('hex', offset, offset + 4);
37+
offset+= 4;
38+
var option;
39+
while ((option = b.readUInt8(offset++, true)) != 255) {
40+
var length = b.readUInt8(offset++, true);
41+
if (option == 50) {
42+
// requested ip address
43+
for (i = 0; i < 4; ++i) {
44+
ret.info.requestedip += b[offset++];
45+
if (i < 3)
46+
ret.info.requestedip += '.';
47+
}
48+
}
49+
else {
50+
offset+= length
51+
}
52+
}
53+
54+
ret.offset = offset;
55+
return ret;
56+
};

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"amazing-dash-button.coffee",
1313
"amazing-dash-button-config-schema.coffee",
1414
"device-config-schema.coffee",
15+
"bootp.js",
1516
"LICENSE",
1617
"HISTORY.md",
1718
"README.md"
@@ -42,7 +43,7 @@
4243
"configSchema": "amazing-dash-button-config-schema.coffee",
4344
"dependencies": {
4445
"cap": "^0.1.2",
45-
"pimatic-plugin-commons": "^0.9.2"
46+
"pimatic-plugin-commons": "^0.9.3"
4647
},
4748
"optionalDependencies": {},
4849
"peerDependencies": {

0 commit comments

Comments
 (0)