SensorStream 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/SensorStream.rb +173 -149
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8c0fac5d01abfb17186ab0f582c555ed09b49ac2
|
4
|
+
data.tar.gz: b3f6a855df09dbaa26dfb80af910bdba4a809524
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af878ffa2bf56e349b50340ab7323fbafdfc748ddb661efa5898610b5eee70ee7b864b10b92c69bbbbaa1265b65c6c7db96700d6b9144107f221de6625f2c675
|
7
|
+
data.tar.gz: 7af61bbeb3fe8e1a1d1fe37885a6b444503d407e23390673512125e90209a954cbbf960daa380bd684fb14721ea0a6f44ac0dd0362f0bf0fb452e38a30d1f3cc
|
data/lib/SensorStream.rb
CHANGED
@@ -7,15 +7,18 @@ require 'pp'
|
|
7
7
|
require 'base64'
|
8
8
|
require 'open3'
|
9
9
|
require 'date'
|
10
|
+
#require 'Zlib'
|
10
11
|
|
11
12
|
module SensorStream
|
12
13
|
attr_accessor :host_name, :port_number
|
13
14
|
|
15
|
+
# Allows the SensorStream server to be overridden
|
14
16
|
def self.set_default_server
|
15
|
-
@host_name = "
|
17
|
+
@host_name = "sensorstream.cloudapp.net"
|
16
18
|
@port_number = 80
|
17
19
|
end
|
18
20
|
|
21
|
+
# Internally used to wrap HTTP puts with the requires headers
|
19
22
|
def self.make_http_post(request = "", body_dict = {}, header_dict = {}, timeout = 600, debug = false)
|
20
23
|
# Ensure that the hostname and port are set
|
21
24
|
self.set_default_server unless !@host_name.nil?
|
@@ -23,35 +26,42 @@ module SensorStream
|
|
23
26
|
# Make sure that, at a minimum, the content type is set in the header
|
24
27
|
header_dict["Content-Type"] = "application/json";
|
25
28
|
|
26
|
-
puts header_dict unless !debug
|
27
|
-
|
28
29
|
uri = URI.parse("http://" + @host_name + request);
|
29
|
-
|
30
|
+
if (debug)
|
31
|
+
puts "POST to #{uri.to_s}"
|
32
|
+
puts "Header: #{header_dict}"
|
33
|
+
puts "Body: #{JSON.generate(body_dict)}"
|
34
|
+
end
|
30
35
|
|
31
36
|
http = Net::HTTP.new(@host_name, @port_number);
|
32
37
|
http.read_timeout = timeout; # 10 minute default timeout
|
33
38
|
http.post(uri.request_uri, JSON.generate(body_dict), header_dict);
|
39
|
+
# This is very important: we're making use of the implied return of ruby, here!!!
|
34
40
|
end
|
35
41
|
|
42
|
+
# Internally used to wrap HTTP gets with the required headers
|
36
43
|
def self.make_http_get(request = "", header_dict = {}, timeout = 600, debug = false)
|
37
44
|
# Ensure that the hostname and port are set
|
38
45
|
self.set_default_server unless !@host_name.nil?
|
39
46
|
|
40
|
-
puts header_dict unless !debug
|
41
47
|
|
42
48
|
uri = URI.parse("http://" + @host_name + request);
|
43
|
-
|
49
|
+
if (debug)
|
50
|
+
puts "GET from #{uri.to_s}"
|
51
|
+
puts "Header: #{header_dict.to_s}"
|
52
|
+
end
|
44
53
|
|
45
54
|
http = Net::HTTP.new(@host_name, @port_number);
|
46
55
|
http.read_timeout = 600; # 10 minute timeout
|
47
56
|
http.get(uri.request_uri, header_dict);
|
57
|
+
# This is very important: we're making use of the implied return of ruby, here!!!
|
48
58
|
end
|
49
59
|
|
50
60
|
# create a new device on the server
|
51
61
|
def self.create_device(user_name, device_name, description)
|
52
|
-
dict = {"
|
53
|
-
"
|
54
|
-
"
|
62
|
+
dict = {"username" => user_name,
|
63
|
+
"devicename" => device_name,
|
64
|
+
"description" => description}
|
55
65
|
|
56
66
|
resp = make_http_post("/device.ashx?create=", dict)
|
57
67
|
|
@@ -62,6 +72,7 @@ module SensorStream
|
|
62
72
|
end
|
63
73
|
end
|
64
74
|
|
75
|
+
# Delete a device with a given key from the server
|
65
76
|
def self.delete_device_with_key(device_key)
|
66
77
|
puts "Deleting device #{device_key}"
|
67
78
|
resp = make_http_post("/device.ashx?delete=device", {}, { "key" => device_key })
|
@@ -74,10 +85,12 @@ module SensorStream
|
|
74
85
|
end
|
75
86
|
end
|
76
87
|
|
88
|
+
# Deletes a device from the server given a ruby object with a key
|
77
89
|
def self.delete_device(device)
|
78
90
|
delete_device_with_key(device.key)
|
79
91
|
end
|
80
92
|
|
93
|
+
# Delete a stream from a device given a streamID and device key
|
81
94
|
def self.delete_stream_with_key(streamID, device_key)
|
82
95
|
puts "Deleting Stream #{streamID}"
|
83
96
|
resp = make_http_post("/stream.ashx?delete=#{streamID}", {}, { "key" => device_key })
|
@@ -90,6 +103,7 @@ module SensorStream
|
|
90
103
|
end
|
91
104
|
end
|
92
105
|
|
106
|
+
# Reads a list of devices from the server
|
93
107
|
def self.get_devices()
|
94
108
|
resp = make_http_get("/device.ashx?getdevices=");
|
95
109
|
|
@@ -97,22 +111,38 @@ module SensorStream
|
|
97
111
|
return nil;
|
98
112
|
else
|
99
113
|
devices = [];
|
100
|
-
JSON.parse(resp.body)
|
101
|
-
devices << SensorStream::Device.new(device["
|
102
|
-
device["
|
103
|
-
device["
|
104
|
-
devices.last.get_streams
|
114
|
+
JSON.parse(resp.body).each { |device|
|
115
|
+
devices << SensorStream::Device.new(device["username"],
|
116
|
+
device["devicename"],
|
117
|
+
device["description"])
|
118
|
+
#devices.last.get_streams
|
105
119
|
}
|
106
120
|
return devices;
|
107
121
|
end
|
108
122
|
end
|
109
123
|
|
124
|
+
# Retreives a device given a device name
|
125
|
+
def self.get_device_by_name(name)
|
126
|
+
devices = SensorStream.get_devices
|
127
|
+
device = nil
|
128
|
+
devices.each { |test_device|
|
129
|
+
if (test_device.device_name == name)
|
130
|
+
return test_device
|
131
|
+
end
|
132
|
+
}
|
133
|
+
|
134
|
+
return nil
|
135
|
+
end
|
136
|
+
|
137
|
+
# Implementation of the device class
|
110
138
|
class Device
|
111
139
|
# Each of the qualities of a device are immutable.
|
112
140
|
# The sensor stream API does not support changing device attributes.
|
113
141
|
attr_reader :user_name, :device_name, :description
|
142
|
+
# It is possible to change the streams and assign a key later, however.
|
114
143
|
attr_accessor :streams, :key
|
115
144
|
|
145
|
+
# Initialize a new SensorStream device object
|
116
146
|
def initialize(newUserName, newDeviceName, newDescription, newStreams = [], newKey = "")
|
117
147
|
@key = newKey
|
118
148
|
@device_name = newDeviceName
|
@@ -121,14 +151,26 @@ module SensorStream
|
|
121
151
|
@streams = newStreams
|
122
152
|
end
|
123
153
|
|
154
|
+
# Create a useful (to a human) representation of the device for printing
|
124
155
|
def to_s
|
125
156
|
"Name: #{@device_name} \n\tUser: #{@user_name} \n\tDescription: #{@description} \n\tKey: #{@key}"
|
126
157
|
end
|
127
158
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
159
|
+
# Create a new stream on this device
|
160
|
+
def create_stream(name, description, elements)
|
161
|
+
|
162
|
+
# Enforce lower-case values on all given element keys
|
163
|
+
tempElements = []
|
164
|
+
elements.each { |element|
|
165
|
+
new_hash = element.each_with_object({}) do |(k, v), h|
|
166
|
+
h[k.downcase] = v
|
167
|
+
end
|
168
|
+
tempElements << new_hash
|
169
|
+
}
|
170
|
+
|
171
|
+
dict = {"name" => name,
|
172
|
+
"description" => description,
|
173
|
+
"streams" => tempElements};
|
132
174
|
|
133
175
|
resp = SensorStream.make_http_post("/stream.ashx?create=", dict, { "key" => @key })
|
134
176
|
|
@@ -136,28 +178,14 @@ module SensorStream
|
|
136
178
|
STDERR.puts "Error creating SensorStream device! (#{resp.code})\n#{resp.body}"
|
137
179
|
return nil
|
138
180
|
else
|
139
|
-
return
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
def create_stream(name, type, units, description)
|
144
|
-
dict = {"Name" => name,
|
145
|
-
"Type" => type,
|
146
|
-
"Units" => units,
|
147
|
-
"Description" => description}
|
148
|
-
|
149
|
-
resp = SensorStream.make_http_post("/stream.ashx?create=", dict, { "key" => @key })
|
150
|
-
|
151
|
-
if (resp.code != "200")
|
152
|
-
STDERR.puts "Error creating SensorStream stream! (#{resp.code})\n#{resp.body}"
|
153
|
-
else
|
154
|
-
Stream.new(self, JSON.parse(resp.body)["StreamID"], name, type, units, description)
|
181
|
+
return Stream.new(self, JSON.parse(resp.body)["streamid"], name, description, tempElements)
|
155
182
|
end
|
156
183
|
end
|
157
184
|
|
158
|
-
|
159
|
-
|
160
|
-
|
185
|
+
# Delete a stream from this device using a stream ID
|
186
|
+
def delete_stream_with_id(streamid)
|
187
|
+
puts "Deleting Stream #{streamid}"
|
188
|
+
resp = SensorStream.make_http_post("/stream.ashx?delete=#{streamid}", {}, { "key" => @key })
|
161
189
|
|
162
190
|
if (resp.code != "200")
|
163
191
|
STDERR.puts "Error deleting stream #{streamID} (#{resp.code})\n{resp.body}"
|
@@ -167,37 +195,32 @@ module SensorStream
|
|
167
195
|
end
|
168
196
|
end
|
169
197
|
|
198
|
+
# Delete a stream from this device using a ruby object
|
170
199
|
def delete_stream(stream)
|
171
|
-
delete_stream_with_id(stream.
|
200
|
+
delete_stream_with_id(stream.streamid)
|
172
201
|
end
|
173
202
|
|
203
|
+
# Delete all the streams belonging to this device
|
174
204
|
def delete_streams
|
175
205
|
puts "Deleting all streams"
|
176
206
|
resp = SensorStream.make_http_post("/device.ashx?delete=streams", {}, { "key" => @key }, 600, true)
|
177
207
|
|
178
208
|
if (resp.code != "200")
|
179
|
-
STDERR.puts "Error deleting stream #{
|
209
|
+
STDERR.puts "Error deleting stream #{streamid} (#{resp.code})\n{resp.body}"
|
180
210
|
false
|
181
211
|
else
|
182
212
|
true
|
183
213
|
end
|
184
214
|
end
|
185
215
|
|
216
|
+
# Get a list of all the streams for this device
|
186
217
|
def get_streams
|
187
218
|
resp = SensorStream.make_http_get("/stream.ashx?getstreams=#{URI::encode(@device_name)}&user=#{URI::encode(@user_name)}")
|
188
219
|
|
189
220
|
if (resp.code == "200")
|
190
221
|
streams = []
|
191
|
-
JSON.parse(resp.body)["
|
192
|
-
|
193
|
-
if(streamDict["Streams"].nil?)
|
194
|
-
streams << Stream.new(self,
|
195
|
-
streamDict["StreamID"], streamDict["Name"],
|
196
|
-
streamDict["Type"], streamDict["Units"],
|
197
|
-
streamDict["Description"])
|
198
|
-
else
|
199
|
-
streams << ComplexStream.new(self, streamDict["StreamID"], streamDict["Name"], streamDict["Description"], streamDict["Streams"])
|
200
|
-
end
|
222
|
+
JSON.parse(resp.body)["streams"].each do |streamDict|
|
223
|
+
streams << Stream.new(self, streamDict["streamid"], streamDict["name"], streamDict["description"], streamDict["streams"])
|
201
224
|
end
|
202
225
|
return streams;
|
203
226
|
else
|
@@ -205,151 +228,152 @@ module SensorStream
|
|
205
228
|
return nil
|
206
229
|
end
|
207
230
|
end
|
231
|
+
|
232
|
+
# Get a stream by name
|
233
|
+
def get_stream_by_name(name)
|
234
|
+
streams = self.get_streams
|
235
|
+
stream = nil
|
236
|
+
streams.each { |test_stream|
|
237
|
+
if (test_stream.name == name)
|
238
|
+
return test_stream
|
239
|
+
end
|
240
|
+
}
|
241
|
+
|
242
|
+
return nil
|
243
|
+
end
|
208
244
|
end
|
209
245
|
|
210
246
|
# The simple stream class is the base class for complex streams
|
211
247
|
class Stream
|
212
248
|
# Each of the qualities of the stream are immutable.
|
213
249
|
# The sensor stream API does not support changing stream attributes.
|
214
|
-
attr_reader :device, :
|
250
|
+
attr_reader :device, :streamid, :name, :description, :elements
|
215
251
|
|
216
|
-
|
217
|
-
|
218
|
-
@
|
219
|
-
@
|
220
|
-
@
|
221
|
-
@
|
222
|
-
@description
|
223
|
-
@
|
224
|
-
end
|
252
|
+
# Initalize the fields of the stream object
|
253
|
+
def initialize(newDevice, newStreamid, newName, newDescription, newElements)
|
254
|
+
@device = newDevice
|
255
|
+
@streamid = newStreamid
|
256
|
+
@name = newName
|
257
|
+
@elements = newElements
|
258
|
+
@description = newDescription
|
259
|
+
@deferredEvents = []
|
260
|
+
end
|
225
261
|
|
262
|
+
# Create a useful (to a human) representation of the device for printing
|
226
263
|
def to_s
|
227
|
-
|
228
|
-
|
229
|
-
|
264
|
+
elementsString = "";
|
265
|
+
@elements.each do |element|
|
266
|
+
elementsString += "\n\t\t#{element["name"]}" +
|
267
|
+
"\n\t\t\tTypes: #{element["type"]}" +
|
268
|
+
"\n\t\t\tUnits: #{element["units"]}"
|
269
|
+
end
|
270
|
+
|
271
|
+
"Name: #{@name} \n\tStreamID: #{@streamid} " +
|
272
|
+
"\n\tElements: #{elementsString} " +
|
273
|
+
"\n\tDescription: #{@description}" +
|
274
|
+
"\n\tDevice: #{device.device_name}"
|
230
275
|
end
|
231
276
|
|
232
|
-
|
233
|
-
|
234
|
-
dict
|
235
|
-
|
236
|
-
|
277
|
+
# Publish an event immediately. If it doesn't succeed, add to the deferred queue.
|
278
|
+
def publish_event(values, time=nil)
|
279
|
+
dict = {"streamid" => @streamid, "values" => values};
|
280
|
+
dict["time"] = time.strftime("%FT%T.%N%:z") unless time.nil?
|
281
|
+
attemptTime = Time.now
|
237
282
|
|
238
283
|
resp = SensorStream.make_http_post("/data.ashx?create=", dict, { "key" => @device.key })
|
239
|
-
|
284
|
+
|
240
285
|
if (resp.code != "200")
|
241
|
-
STDERR.puts "Error publishing SensorStream event! (#{resp.code})\n#{resp.body}"
|
242
|
-
|
286
|
+
STDERR.puts "Error publishing SensorStream event! (#{resp.code})\n#{resp.body} -- deferring message"
|
287
|
+
publish_event_deferred(values, attemptTime)
|
288
|
+
return nil;
|
243
289
|
else
|
244
|
-
DateTime.strptime(JSON.parse(resp.body)["
|
290
|
+
DateTime.strptime(JSON.parse(resp.body)["time"],"%FT%T.%N%:z")
|
245
291
|
end
|
246
292
|
end
|
247
293
|
|
248
|
-
|
294
|
+
# Add an event to the deferred message queue
|
295
|
+
def publish_event_deferred(values, time=nil)
|
249
296
|
time ||= Time.now
|
250
|
-
|
251
|
-
|
252
|
-
|
297
|
+
|
298
|
+
if(@deferred_events.nil?)
|
299
|
+
@deferred_events = []
|
300
|
+
end
|
301
|
+
|
302
|
+
@deferred_events << { "streamid" => @streamid,
|
303
|
+
"values" => values,
|
304
|
+
"time" => time.strftime("%FT%T.%N%:z") }
|
253
305
|
end
|
254
306
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
307
|
+
# Publish all the deferred events (if there are any)
|
308
|
+
def publish_deferred_events
|
309
|
+
# Ensure that there are deferred messages to send
|
310
|
+
if(@deferred_events.nil? || @deferred_events.empty?)
|
311
|
+
@deferred_events = [] unless !@deferred_events.nil?
|
312
|
+
puts "No events to publish"
|
313
|
+
if (@deferred_events.nil?)
|
314
|
+
puts "Deferred events nil."
|
315
|
+
elsif (@deferred_events.empty?)
|
316
|
+
puts "Deferred events empty."
|
317
|
+
end
|
318
|
+
return 0
|
260
319
|
end
|
261
320
|
|
262
|
-
resp = SensorStream.make_http_post("/data.ashx?create=", @
|
321
|
+
resp = SensorStream.make_http_post("/data.ashx?create=", @deferred_events, { "key" => @device.key })
|
263
322
|
|
264
323
|
# If the send failed, keep the messages
|
265
324
|
if resp.code != "200"
|
266
|
-
puts "Unable to publish
|
325
|
+
puts "Unable to publish deferred events, keeping events."
|
326
|
+
return 0
|
267
327
|
else
|
268
|
-
count = @
|
269
|
-
@
|
328
|
+
count = @deferred_events.count
|
329
|
+
@deferred_events = []
|
270
330
|
return count
|
271
331
|
end
|
272
332
|
end
|
273
333
|
|
334
|
+
# Download a list of events from the server from this stream
|
274
335
|
def get_events(count = 1, start_time = nil, end_time = nil)
|
275
|
-
base = "/data.ashx?getdata=#{@
|
336
|
+
base = "/data.ashx?getdata=#{@streamid}"
|
276
337
|
base += "&start=#{start_time.strftime("%FT%T.%N%:z")}" unless start_time.nil?
|
277
338
|
base += "&end=#{ end_time.strftime("%FT%T.%N%:z")}" unless end_time.nil?
|
278
339
|
base += "&count=#{count}"
|
279
340
|
|
280
|
-
resp = SensorStream.make_http_get(base)
|
341
|
+
resp = SensorStream.make_http_get(base, {"Accept-Encoding" => "gzip"}, 600, true)
|
281
342
|
|
282
343
|
if resp.code != "200"
|
283
|
-
puts
|
344
|
+
STDERR.puts "Unable to load events from stream: #{resp.body}"
|
284
345
|
return nil
|
285
346
|
end
|
286
347
|
|
348
|
+
if (resp["Content-Encoding"] == "gzip")
|
349
|
+
puts "Content was zipped, inflating"
|
350
|
+
content = Zlib::Inflate.inflate(resp.body)
|
351
|
+
puts resp.body
|
352
|
+
elsif
|
353
|
+
content = resp.body
|
354
|
+
end
|
355
|
+
|
287
356
|
# We're only interested in the values and their timestamps
|
288
357
|
# but the server gives us the device information anyway
|
289
358
|
# Return only the Data array from the dictionary
|
290
|
-
|
291
|
-
data =
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
#puts event_dict
|
298
|
-
}
|
299
|
-
return data
|
300
|
-
else
|
301
|
-
return [event_dict["Time"] = DateTime.strptime(event_dict["Time"],"%FT%T.%N%:z")];
|
302
|
-
end
|
303
|
-
end
|
304
|
-
end
|
305
|
-
|
306
|
-
class ComplexStream < Stream
|
307
|
-
#attr_protected :type, :units
|
308
|
-
attr_reader :elements
|
309
|
-
def initialize(newDevice, newStreamID, newName, newDescription, newElements)
|
310
|
-
@device = newDevice
|
311
|
-
@streamID = newStreamID
|
312
|
-
@name = newName
|
313
|
-
@elements = newElements
|
314
|
-
@description = newDescription
|
315
|
-
@deferedEvents = []
|
316
|
-
end
|
317
|
-
|
318
|
-
def to_s
|
319
|
-
elementsString = "";
|
320
|
-
@elements.each do |element|
|
321
|
-
elementsString += "\n\t\t#{element["Name"]}" +
|
322
|
-
"\n\t\t\tTypes: #{element["Type"]}" +
|
323
|
-
"\n\t\t\tUnits: #{element["Units"]}"
|
324
|
-
end
|
325
|
-
|
326
|
-
"Name: #{@name} \n\tStreamID: #{@streamID} " +
|
327
|
-
"\n\tElements: #{elementsString} " +
|
328
|
-
"\n\tDescription: #{@description}" +
|
329
|
-
"\n\tDevice: #{device.device_name}"
|
330
|
-
end
|
331
|
-
|
332
|
-
def publish_event(values, time=nil)
|
333
|
-
dict = {"StreamID" => @streamID, "Values" => values};
|
334
|
-
dict["Time"] = time.strftime("%FT%T.%N%:z") unless time.nil?
|
335
|
-
|
336
|
-
#puts dict
|
359
|
+
streams = (JSON.parse(resp.body)["streams"])
|
360
|
+
data = nil;
|
361
|
+
streams.each { |stream|
|
362
|
+
if (stream["streamid"] == @streamid)
|
363
|
+
data = stream["data"]
|
364
|
+
end
|
365
|
+
}
|
337
366
|
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
STDERR.puts "Error publishing SensorStream event! (#{resp.code})\n#{resp.body}"
|
342
|
-
return nil;
|
343
|
-
else
|
344
|
-
DateTime.strptime(JSON.parse(resp.body)["Time"],"%FT%T.%N%:z")
|
367
|
+
if (data.nil?)
|
368
|
+
STDERR.puts "Can't find data in get events for this stream"
|
369
|
+
return nil
|
345
370
|
end
|
371
|
+
|
372
|
+
data.each { |event_dict|
|
373
|
+
# Replace the time string with Ruby time objects
|
374
|
+
event_dict["time"] = DateTime.strptime(event_dict["time"],"%FT%T.%N%:z")
|
375
|
+
}
|
376
|
+
return data
|
346
377
|
end
|
347
|
-
|
348
|
-
def publish_event_defered(values, time=nil)
|
349
|
-
time ||= Time.now
|
350
|
-
@defered_events << { "StreamID" => @streamID,
|
351
|
-
"Values" => values,
|
352
|
-
"Time" => time.strftime("%FT%T.%N%:z") }
|
353
|
-
end
|
354
|
-
end
|
378
|
+
end
|
355
379
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: SensorStream
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- William Dillon
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-05-15 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A Ruby implementation of the SensorStream API
|
14
14
|
email: william@housedillon.com
|
@@ -37,7 +37,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
37
37
|
version: '0'
|
38
38
|
requirements: []
|
39
39
|
rubyforge_project:
|
40
|
-
rubygems_version: 2.0.
|
40
|
+
rubygems_version: 2.0.14
|
41
41
|
signing_key:
|
42
42
|
specification_version: 4
|
43
43
|
summary: SensorStream API
|