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.
@@ -0,0 +1,6 @@
1
+ module TempoIQ
2
+ module Constants
3
+ VERSION = "1.0.0"
4
+ TRUSTED_CERT_FILE = File.join(File.dirname(__FILE__), "..", "trusted-certs.crt")
5
+ end
6
+ end
@@ -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