tempoiq 1.0.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 +7 -0
- data/Gemfile +6 -0
- data/README.md +35 -0
- data/Rakefile +28 -0
- data/lib/tempoiq.rb +7 -0
- data/lib/tempoiq/client.rb +450 -0
- data/lib/tempoiq/constants.rb +6 -0
- data/lib/tempoiq/models/bulk_write.rb +31 -0
- data/lib/tempoiq/models/cursor.rb +48 -0
- data/lib/tempoiq/models/datapoint.rb +28 -0
- data/lib/tempoiq/models/delete_summary.rb +12 -0
- data/lib/tempoiq/models/device.rb +40 -0
- data/lib/tempoiq/models/find.rb +18 -0
- data/lib/tempoiq/models/multi_status.rb +27 -0
- data/lib/tempoiq/models/pipeline.rb +86 -0
- data/lib/tempoiq/models/query.rb +20 -0
- data/lib/tempoiq/models/read.rb +21 -0
- data/lib/tempoiq/models/row.rb +32 -0
- data/lib/tempoiq/models/search.rb +17 -0
- data/lib/tempoiq/models/selection.rb +57 -0
- data/lib/tempoiq/models/sensor.rb +28 -0
- data/lib/tempoiq/models/single.rb +17 -0
- data/lib/tempoiq/remoter/http_result.rb +42 -0
- data/lib/tempoiq/remoter/live_remoter.rb +70 -0
- data/lib/tempoiq/remoter/stubbed_remoter.rb +54 -0
- data/lib/trusted-certs.crt +360 -0
- data/test/client_test.rb +629 -0
- data/test/integration/integration-credentials.yml +5 -0
- data/test/integration/test_live_client.rb +19 -0
- data/test/unit/test_cursor.rb +116 -0
- data/test/unit/test_stubbed_client.rb +15 -0
- metadata +136 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
# Used to write DataPoints into your TempoIQ backend.
|
3
|
+
class BulkWrite
|
4
|
+
def initialize
|
5
|
+
@writes = Hash.new do |sensors, device_key|
|
6
|
+
sensors[device_key] = Hash.new do |points, sensor_key|
|
7
|
+
points[sensor_key] = []
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Alias for #add
|
13
|
+
def <<(device_key, sensor_key, datapoint)
|
14
|
+
add(device_key, sensor_key, datapoint)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add a DataPoint to the request
|
18
|
+
#
|
19
|
+
# * +device_key+ [String] - The device key to write to
|
20
|
+
# * +sensor_key+ [String] - The sensor key within the device to write to
|
21
|
+
# * +datapoint+ [DataPoint] - The datapoint to write
|
22
|
+
def add(device_key, sensor_key, datapoint)
|
23
|
+
@writes[device_key][sensor_key] << datapoint.to_hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_hash
|
27
|
+
@writes
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module TempoIQ
|
5
|
+
# Cursor is an abstraction over a sequence / stream of objects. It
|
6
|
+
# uses lazy iteration to transparently fetch segments of data from
|
7
|
+
# the server.
|
8
|
+
#
|
9
|
+
# It implements the Enumerable interface, which means convenience functions
|
10
|
+
# such as Enumerable#to_a are available if you know you're working with a
|
11
|
+
# small enough segment of data that can reasonably fit in memory.
|
12
|
+
class Cursor
|
13
|
+
PAGE_LINK = "next_page"
|
14
|
+
NEXT_QUERY = "next_query"
|
15
|
+
|
16
|
+
attr_reader :remoter, :route, :query, :headers, :segment_key
|
17
|
+
|
18
|
+
include Enumerable
|
19
|
+
|
20
|
+
def initialize(klass, remoter, route, query, headers = {}, segment_key = "data")
|
21
|
+
@klass = klass
|
22
|
+
@remoter = remoter
|
23
|
+
@route = route
|
24
|
+
@query = query
|
25
|
+
@headers = headers
|
26
|
+
@segment_key = segment_key
|
27
|
+
end
|
28
|
+
|
29
|
+
def each
|
30
|
+
segment = nil
|
31
|
+
until segment == nil && query == nil do
|
32
|
+
json = get_segment(JSON.dump(query.to_hash))
|
33
|
+
segment = json[segment_key]
|
34
|
+
segment.each { |item| yield @klass.from_hash(item) }
|
35
|
+
segment = nil
|
36
|
+
@query = json.fetch(PAGE_LINK, {})[NEXT_QUERY]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def get_segment(next_query)
|
43
|
+
remoter.get(route, next_query, headers).on_success do |result|
|
44
|
+
JSON.parse(result.body)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module TempoIQ
|
4
|
+
# The core type of TempoIQ. Holds a timestamp and value.
|
5
|
+
class DataPoint
|
6
|
+
# The timestamp of the datapoint [Time]
|
7
|
+
attr_reader :ts
|
8
|
+
|
9
|
+
# The value of the datapoint [Fixnum / Float]
|
10
|
+
attr_reader :value
|
11
|
+
|
12
|
+
def initialize(ts, value)
|
13
|
+
@ts = ts
|
14
|
+
@value = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
{
|
19
|
+
't' => ts.iso8601(3),
|
20
|
+
'v' => value
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.from_hash(hash)
|
25
|
+
new(Time.parse(hash['t']), m['v'])
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
# When deleting multiple objects from a TempoIQ backend, return
|
3
|
+
# information about what was actually deleted.
|
4
|
+
class DeleteSummary
|
5
|
+
# Number of objects deleted in the call
|
6
|
+
attr_reader :deleted
|
7
|
+
|
8
|
+
def initialize(deleted)
|
9
|
+
@deleted = deleted
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'tempoiq/models/sensor'
|
2
|
+
|
3
|
+
module TempoIQ
|
4
|
+
# The top level container for a group of sensors.
|
5
|
+
class Device
|
6
|
+
# The primary key of the device [String]
|
7
|
+
attr_reader :key
|
8
|
+
|
9
|
+
# Human readable name of the device [String] EG - "My Device"
|
10
|
+
attr_accessor :name
|
11
|
+
|
12
|
+
# Indexable attributes. Useful for grouping related Devices.
|
13
|
+
# EG - {'location' => '445-w-Erie', 'model' => 'TX75', 'region' => 'Southwest'}
|
14
|
+
attr_accessor :attributes
|
15
|
+
|
16
|
+
# Sensors attached to the device [Array] (Sensor)
|
17
|
+
attr_accessor :sensors
|
18
|
+
|
19
|
+
def initialize(key, name = "", attributes = {}, *sensors)
|
20
|
+
@key = key
|
21
|
+
@name = name
|
22
|
+
@attributes = attributes
|
23
|
+
@sensors = sensors
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.from_hash(hash)
|
27
|
+
new(hash['key'], hash['name'], hash['attributes'],
|
28
|
+
*hash['sensors'].map { |s| Sensor.new(s['key'], s['name'], s['attributes']) })
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash
|
32
|
+
{
|
33
|
+
'key' => key,
|
34
|
+
'name' => name,
|
35
|
+
'attributes' => attributes,
|
36
|
+
'sensors' => sensors.map(&:to_hash)
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
class Find
|
3
|
+
attr_reader :name, :limit
|
4
|
+
|
5
|
+
def initialize(limit = nil)
|
6
|
+
@name = "find"
|
7
|
+
@limit = limit
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_hash
|
11
|
+
hash = {
|
12
|
+
"quantifier" => "all"
|
13
|
+
}
|
14
|
+
hash["limit"] = limit if limit
|
15
|
+
hash
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
# MultiStatus is used in cases where an operation might partially succeed
|
3
|
+
# and partially fail. It provides several helper functions to introspect the
|
4
|
+
# failure and take appropriate action. (Log failure, resend DataPoints, etc.)
|
5
|
+
class MultiStatus
|
6
|
+
attr_reader :status
|
7
|
+
|
8
|
+
def initialize(status = nil)
|
9
|
+
@status = status
|
10
|
+
end
|
11
|
+
|
12
|
+
# Was the request a total success?
|
13
|
+
def success?
|
14
|
+
status.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
# Did the request have partial failures?
|
18
|
+
def partial_success?
|
19
|
+
!success?
|
20
|
+
end
|
21
|
+
|
22
|
+
# Retrieve the failures, key => message [Hash]
|
23
|
+
def failures
|
24
|
+
Hash[status.map { |device_key, v| [device_key, v["message"]] } ]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
# Used to transform a stream of devices using a list of
|
3
|
+
# function transformations.
|
4
|
+
class Pipeline
|
5
|
+
attr_reader :functions
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@functions = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# DataPoint aggregation
|
12
|
+
#
|
13
|
+
# * +function+ [Symbol] - Function to aggregate by. One of:
|
14
|
+
# * count - The number of datapoints across sensors
|
15
|
+
# * sum - Summation of all datapoints across sensors
|
16
|
+
# * mult - Multiplication of all datapoints across sensors
|
17
|
+
# * min - The smallest datapoint value across sensors
|
18
|
+
# * max - The largest datapoint value across sensors
|
19
|
+
# * stddev - The standard deviation of the datapoint values across sensors
|
20
|
+
# * ss - Sum of squares of all datapoints across sensors
|
21
|
+
# * range - The maximum value less the minimum value of the datapoint values across sensors
|
22
|
+
# * percentile,N (where N is what percentile to calculate) - Percentile of datapoint values across sensors
|
23
|
+
def aggregate(function)
|
24
|
+
functions << {
|
25
|
+
"name" => "aggregation",
|
26
|
+
"arguments" => [function.to_s]
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Rollup a stream of DataPoints to a given period
|
31
|
+
#
|
32
|
+
# * +period+ [String] - The duration of each rollup. Specified by:
|
33
|
+
# * A number and unit of time: EG - '1min' '10days'.
|
34
|
+
# * A valid ISO8601 duration
|
35
|
+
# * +function+ [Symbol] - Function to rollup by. One of:
|
36
|
+
# * count - The number of datapoints in the period
|
37
|
+
# * sum - Summation of all datapoint values in the period
|
38
|
+
# * mult - Multiplication of all datapoint values in the period
|
39
|
+
# * min - The smallest datapoint value in the period
|
40
|
+
# * max - The largest datapoint value in the period
|
41
|
+
# * stddev - The standard deviation of the datapoint values in the period
|
42
|
+
# * ss - Sum of squares of all datapoint values in the period
|
43
|
+
# * range - The maximum value less the minimum value of the datapoint values in the period
|
44
|
+
# * percentile,N (where N is what percentile to calculate) - Percentile of datapoint values in period
|
45
|
+
# * +start+ [Time] - The beginning of the rollup interval
|
46
|
+
def rollup(period, function, start)
|
47
|
+
functions << {
|
48
|
+
"name" => "rollup",
|
49
|
+
"arguments" => [
|
50
|
+
function.to_s,
|
51
|
+
period,
|
52
|
+
start.iso8601(3)
|
53
|
+
]
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# Interpolate missing data within a sensor, based on
|
58
|
+
#
|
59
|
+
# * +period+ [String] - The duration of each rollup. Specified by:
|
60
|
+
# * A number and unit of time: EG - '1min' '10days'.
|
61
|
+
# * A valid ISO8601 duration
|
62
|
+
# * +function+ [Symbol] - The type of interpolation to perform. One of:
|
63
|
+
# * linear - Perform linear interpolation
|
64
|
+
# * zoh - Zero order hold interpolation
|
65
|
+
# * +start+ [Time] - The beginning of the interpolation range
|
66
|
+
# * +stop+ [Time] - The end of the interpolation range
|
67
|
+
def interpolate(period, interpolation_function, start, stop)
|
68
|
+
functions << {
|
69
|
+
"name" => "interpolate",
|
70
|
+
"arguments" => [
|
71
|
+
interpolation_function.to_s,
|
72
|
+
period,
|
73
|
+
start.iso8601(3),
|
74
|
+
stop.iso8601(3)
|
75
|
+
]
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_hash
|
80
|
+
{
|
81
|
+
"functions" => functions
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
class Query
|
3
|
+
attr_reader :search, :action, :pipeline
|
4
|
+
|
5
|
+
def initialize(search, action, pipeline = nil)
|
6
|
+
@search = search
|
7
|
+
@action = action
|
8
|
+
@pipeline = pipeline
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_hash
|
12
|
+
hash = {
|
13
|
+
"search" => search.to_hash,
|
14
|
+
action.name => action.to_hash
|
15
|
+
}
|
16
|
+
hash["fold"] = pipeline.to_hash if pipeline
|
17
|
+
hash
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
class Read
|
3
|
+
attr_reader :name, :start, :stop, :limit
|
4
|
+
|
5
|
+
def initialize(start, stop, limit = nil)
|
6
|
+
@name = "read"
|
7
|
+
@start = start
|
8
|
+
@stop = stop
|
9
|
+
@limit = limit
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_hash
|
13
|
+
hash = {
|
14
|
+
"start" => start.iso8601(3),
|
15
|
+
"stop" => stop.iso8601(3)
|
16
|
+
}
|
17
|
+
hash["limit"] = limit if limit
|
18
|
+
hash
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
# Represents all the data found at a single timestamp.
|
3
|
+
#
|
4
|
+
# The hierarchy looks like:
|
5
|
+
# - timestamp
|
6
|
+
# - device_key
|
7
|
+
# - sensor_key => value
|
8
|
+
class Row
|
9
|
+
# Timestamp of the row
|
10
|
+
attr_reader :ts
|
11
|
+
|
12
|
+
# Data at the timestamp [Hash]
|
13
|
+
#
|
14
|
+
# Looks like: {"device1" => {"sensor1" => 1.23, "sensor2" => 2.34}}
|
15
|
+
attr_reader :values
|
16
|
+
|
17
|
+
def initialize(ts, values)
|
18
|
+
@ts = ts
|
19
|
+
@values = values
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.from_hash(hash)
|
23
|
+
new(hash['t'], hash['data'])
|
24
|
+
end
|
25
|
+
|
26
|
+
# Convenience method to select a single (device, sensor)
|
27
|
+
# value from within the row.
|
28
|
+
def value(device_key, key)
|
29
|
+
@values[device_key][key]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
class Search
|
3
|
+
attr_reader :select, :selection
|
4
|
+
|
5
|
+
def initialize(select, selection)
|
6
|
+
@select = select
|
7
|
+
@selection = selection
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_hash
|
11
|
+
{
|
12
|
+
"select" => select,
|
13
|
+
"filters" => selection.to_hash
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module TempoIQ
|
2
|
+
# Selection is a core concept that defines how you can retrieve
|
3
|
+
# a group of related objects by common metadata. For Device and
|
4
|
+
# Sensor selection, it is primarily used to drive Device searching
|
5
|
+
# (Client#list_devices) and for driving reads (Client#read).
|
6
|
+
#
|
7
|
+
# TempoIQ currently maps selection onto the following domain objects
|
8
|
+
#
|
9
|
+
# - Device
|
10
|
+
# - Sensor
|
11
|
+
#
|
12
|
+
# TempoIQ currently supports filtering objects by the following metadata:
|
13
|
+
#
|
14
|
+
# ==== Simple selectors:
|
15
|
+
#
|
16
|
+
# - +key+
|
17
|
+
# - +attribute_key+
|
18
|
+
# - +attributes+
|
19
|
+
#
|
20
|
+
# ==== Compound selectors:
|
21
|
+
# - +or+
|
22
|
+
# - +and+
|
23
|
+
#
|
24
|
+
# ==== Simple Examples
|
25
|
+
#
|
26
|
+
# # Select devices with the key 'heatpump4549' (should return an Array of size 1)
|
27
|
+
# {:devices => {:key => 'heatpump4549'}}
|
28
|
+
#
|
29
|
+
# # Select devices that are in buildings
|
30
|
+
# {:devices => {:attribute_key => 'building'}}
|
31
|
+
#
|
32
|
+
# # Select devices that are in building '445-w-erie'
|
33
|
+
# {:devices => {:attributes => {'building' => '445-w-erie'}}}
|
34
|
+
#
|
35
|
+
# # Select devices in buildings that have TX455 model sensors
|
36
|
+
# {:devices => {:attribute_key => 'building'},
|
37
|
+
# :sensors => {:attributes => {'model' => 'TX455'}}}
|
38
|
+
#
|
39
|
+
# ==== Compound examples
|
40
|
+
#
|
41
|
+
# # Select devices with key 'heatpump4549' or 'heatpump5789'
|
42
|
+
# {:devices => {:or => [{:key => 'heatpump4549'}, {:key => 'heatpump5789'}]}}
|
43
|
+
#
|
44
|
+
# # Select devices in buildings in the Evanston region
|
45
|
+
# {:devices => {:and => [{:attribute_key => 'building'}, {:attributes => {'region' => 'Evanston'}}]}}
|
46
|
+
class Selection
|
47
|
+
attr_reader :select, :filter
|
48
|
+
|
49
|
+
def initialize(select, filter = {})
|
50
|
+
@select = select
|
51
|
+
@filter = filter
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_hash
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|