Browse files

Extracted xively-rb-connector to its own gem structure.

  • Loading branch information...
1 parent 30019a2 commit 06764f53bffc4439abf58ebed2974aa95c33e910 @jwtd committed Mar 31, 2014
View
16 .gitignore
@@ -9,11 +9,6 @@
/test/version_tmp/
/tmp/
-## Specific to RubyMotion:
-.dat*
-.repl_history
-build/
-
## Documentation cache and generated files:
/.yardoc/
/_yardoc/
@@ -26,9 +21,14 @@ build/
# for a library or gem, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
-# Gemfile.lock
-# .ruby-version
-# .ruby-gemset
+Gemfile.lock
+.ruby-version
+.ruby-gemset
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
.rvmrc
+
+## Specific to RubyMotion:
+.dat*
+.repl_history
+build/
View
4 Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in ekm-meter.gemspec
+gemspec
View
71 README.md
@@ -2,3 +2,74 @@ xively-rb-connector
===================
Ruby gem that provides an interface to Xively by extending xively-rb with convenience functions such Device.find_by_id, a datastream compression (only saves datapoints when value changes), a datapoint recording buffer, etc.
+
+
+```ruby
+
+require 'xively-rb-connector'
+
+d = XivelyConnector.find_device_by_id('123456789', 'ACCOUNT-OR-DEVICE-API-KEY-HERE')
+
+d.id # 123456789
+d.feed # https://api.xively.com/v2/feeds/123456789.json
+d.auto_feed_url # https://api.xively.com/v2/feeds/123456789.json
+
+d.title # My Meter
+d.device_serial: # FOOBAR123456
+d.description # The xively on xively.com
+d.tags # ["Gas", "Power", "Propane", "Water"]
+d.creator # https://xively.com/users/foo
+d.email # your.email@gmail.com
+d.website # http://www.your-website.com
+d.icon:
+
+d.created # 2014-01-01 16:36:30 UTC
+d.updated # 2014-03-31 05:02:25 UTC
+d.private # true
+d.is_private? # true
+d.status # live
+d.is_frozen? # false
+
+# Access location data
+d.has_location? # true
+d.location #<OpenStruct name="6004 Bedfordshire Dr, Raleigh NC", latitude=35.7069733120845, longitude=-78.7572026183472, elevation="354 feet", exposure=nil, disposition=nil, waypoints=nil, domain="physical">
+d.location_name # The location name on
+d.location_domain # physical
+d.location_lon # -78.12343456
+d.location_lat # 35.12343454
+d.location_ele # 354 feet
+d.location_exposure
+d.location_disposition
+d.location_waypoints
+
+# Examine datastreams
+d.datastream_ids ["Amps", "Propane", "Volts", "Watts", "Well"]
+d.datastream_values: ["Amps in Amps (A) = 0", "Propane in Cubic Feet (cft) = 0", "Volts in Volts (V) = 9", "Watts in Watts (W) = 0", "Well in Cubic Feet (cft) = 0"]
+d.has_channel?('Volts') true
+
+# Access datastreams by channel name
+d['Volts'] #<XivelyConnector::Datastream:0x007ff62a137b80>
+d['Volts'].current_value 9
+ds = d['Volts']
+ds.id Volts
+ds.current_value 9
+ds.tags Power
+ds.min_value 0.0
+ds.max_value 25.0
+ds.unit_label Volts
+ds.unit_symbol V
+
+# Queue new datapoints using the shift operator
+ds.datapoints.size 0
+ds << Xively::Datapoint.new(:at => Time.now(), :value => "10")
+ds << Xively::Datapoint.new(:at => Time.now(), :value => "15")
+ds << Xively::Datapoint.new(:at => Time.now(), :value => "25")
+ds.datapoints.size 3
+
+# Save datapoints to xively.com
+ds.only_saves_changes? true # Will only queue a datapoint if its value is different from the previous datapoint's which is also the current_value
+ds.datapoint_buffer_size 60 # Will auto-save to feed when 60 points are added
+ds.save_datapoints
+ds.datapoints.size 0
+
+```
View
1 Rakefile
@@ -0,0 +1 @@
+require "bundler/gem_tasks"
View
31 lib/xively-rb-connector.rb
@@ -0,0 +1,31 @@
+module XivelyConnector
+
+ @@connection = nil
+
+ # Lookup methods delegate to class methods
+ def self.connection
+ @@connection
+ end
+
+ # Lookup methods delegate to class methods
+ def self.connect(options)
+ raise "A connection can not be established without an :api_key" unless options[:api_key]
+ @@connection = Connection.new(options)
+ end
+
+ # Lookup methods delegate to class methods
+ def self.find_device_by_id(id, api_key=nil)
+ self.connect(:api_key => api_key) unless api_key.nil?
+ Device.find_by_id(id)
+ end
+
+ # Base XivelyConnector exception class
+ class XivelyConnectorError < ::Exception; end
+
+end
+
+require "xively-rb-connector/version"
+require "xively-rb-connector/logging"
+require "xively-rb-connector/connection"
+require "xively-rb-connector/device"
+require "xively-rb-connector/datastream"
View
39 lib/xively-rb-connector/connection.rb
@@ -0,0 +1,39 @@
+require 'xively-rb'
+
+module XivelyConnector
+
+ class Connection < Xively::Client
+ include XivelyConnector::Logging
+
+ format :json
+
+ # Mix in the ability to log
+ include XivelyConnector::Logging
+
+ def initialize(options)
+ @logger = options[:logger] || logger
+ @logger.debug "XivelyConnector::Connection initialize"
+ super(options[:api_key])
+ end
+
+ #Set HTTParty params that we need to set after initialize is called
+ #These params come from @options within initialize and include the following:
+ #:ssl_ca_file - SSL CA File for SSL connections
+ #:format - 'json', 'xml', 'html', etc. || Defaults to 'xml'
+ #:format_header - :format Header string || Defaults to 'application/xml'
+ #:pem_cert - /path/to/a/pem_formatted_certificate.pem for SSL connections
+ #:pem_cert_pass - plaintext password, not recommended!
+ def set_httparty_options(options={})
+ if options[:ssl_ca_file]
+ ssl_ca_file opts[:ssl_ca_file]
+ if options[:pem_cert_pass]
+ pem File.read(options[:pem_cert]), options[:pem_cert_pass]
+ else
+ pem File.read(options[:pem_cert])
+ end
+ end
+ end
+
+ end
+
+end
View
84 lib/xively-rb-connector/datastream.rb
@@ -0,0 +1,84 @@
+require 'xively-rb'
+require 'json'
+require 'bigdecimal'
+
+module XivelyConnector
+
+ # Extend https://github.com/xively-rb-connector/xively-rb
+ class Datastream < Xively::Datastream
+
+ attr_reader :device
+ attr :datapoint_buffer_size, :only_save_changes
+
+ # Mix in the ability to log
+ include XivelyConnector::Logging
+
+ def initialize(options)
+ @logger = options[:logger] || logger
+ @logger.debug "XivelyConnector::Datastream initialize"
+
+ raise "An instance of XivelyConnector::Device must be provided to create a datastream" unless options[:device]
+ raise "The data block for this datastream is missing" unless options[:data]
+
+ # Capture parent and initialize Xively::Datastream
+ @device = options[:device]
+ data = options[:data]
+ super(data)
+
+ # Set the default buffer size (1 reading per minute)
+ @datapoint_buffer_size = options[:datapoint_buffer_size] || 60
+ @only_save_changes = options[:only_save_changes] || true
+
+ # Initialize Xively::Datastream's datapoint array to allow shift style loading
+ @datapoints = []
+
+ # Fix the unit attribute assignments
+ @unit_symbol = data['unit']['symbol']
+ @unit_label = data['unit']['label']
+
+ # Cast the current value as a BigDecimal for casting strings and comparing to ints and floats
+ @current_value = 0 if current_value.nil?
+
+ @logger.info "Initialized Datastream #{@id} (#{@unit_symbol})"
+
+ end
+
+ def only_saves_changes?
+ only_save_changes
+ end
+
+ # Have shift operator load the datapoint into the datastream's datapoints array
+ # If only_save_changes is true, ignore datapoints whose value is the same as the current value
+ # If the datapoints array has exceeded the datapoint_buffer_size, send them to Xively
+ def <<(datapoint)
+ if only_save_changes and BigDecimal.new(datapoint.value) == BigDecimal.new(current_value)
+ @logger.debug "Ignoring datapoint from #{datapoint.at} because value did not change from #{current_value}"
+ else
+ @current_value = datapoint.value
+ datapoints << datapoint
+ @logger.debug "Queuing datapoint from #{datapoint.at} with value #{current_value}"
+ end
+ save_datapoints if datapoints.size >= @datapoint_buffer_size
+ end
+
+ # Send the queued datapoints array to Xively
+ def save_datapoints
+ @logger.debug "Saving #{datapoints.size} datapoints to the #{id} datastream"
+ response = XivelyConnector.connection.post("/v2/feeds/#{device.id}/datastreams/#{id}/datapoints",
+ :body => {:datapoints => datapoints}.to_json)
+ if response.success?
+ clear_datapoints
+ response
+ else
+ logger.error response.response
+ raise response.response
+ end
+ end
+
+ # Resets the datapoints
+ def clear_datapoints
+ @datapoints = []
+ end
+
+ end
+end
View
130 lib/xively-rb-connector/device.rb
@@ -0,0 +1,130 @@
+require 'xively-rb'
+require 'json'
+require 'ostruct'
+
+
+module XivelyConnector
+
+# Extend https://github.com/xively-rb-connector/xively-rb
+class Device < Xively::Feed
+
+ # Mix in the ability to log
+ include XivelyConnector::Logging
+
+ # Connect to a device by ID
+ def self.find_by_id(id)
+ XivelyConnector.connection.logger.debug "Device.find_by_id(#{id})"
+ response = XivelyConnector.connection.get("/v2/feeds/#{id}.json")
+ if response.success?
+ self.new(:response => response)
+ else
+ logger.error response.response
+ response.response
+ end
+ end
+
+ # Converts the Xively dates to ruby DateTimnes and make the datastreams accessible via hash syntax
+ def initialize(options)
+
+ # Create logger
+ @logger = options[:logger] || logger
+ @logger.debug "XivelyConnector::Device initialize"
+
+ # initialize parent
+ data = options[:response]
+ super(options[:response])
+
+ # Convert date strings to ruby dates
+ @created = Time.iso8601(created) # 2014-03-28T16:36:30.651731Z
+ @updated = Time.iso8601(updated) # 2014-03-28T16:45:05.206472Z
+
+ # xively-rb doesn't set location attributes correctly, so do so here
+ if data['location']
+ loc = data['location']
+ @has_location = true
+ @location_name = loc['name'] if
+ @location_domain = loc['domain'] if loc['domain']
+ @location_lon = loc['lon'] if loc['lon']
+ @location_lat = loc['lat'] if loc['lat']
+ @location_ele = loc['ele'] if loc['ele']
+ @location_exposure = loc['exposure'] if loc['exposure']
+ @location_disposition = loc['disposition'] if loc['disposition']
+ @location_waypoints = loc['waypoints'] if loc['waypoints']
+ end
+
+ # Setup enhanced datastreams
+ @datastream_ref = {}
+ datastreams.each { |ds| @datastream_ref[ds.id] = ds }
+
+ @logger.info "Initialized Xively Device #{@id} with #{datastreams.size} datastreams"
+
+ end
+
+ # Support bracket notation for retriving data streams
+ def [](channel_id)
+ @datastream_ref[channel_id]
+ end
+
+ # Override the datastreams function so that it creates XivelyConnector::Datastreams which extend the standard one
+ def datastreams=(array)
+ return unless array.is_a?(Array)
+ @datastreams = []
+ array.each do |datastream|
+ if datastream.is_a?(Datastream)
+ @datastreams << datastream
+ elsif datastream.is_a?(Hash)
+ #@datastreams << Datastream.new(datastream)
+ @datastreams << XivelyConnector::Datastream.new(:device => self, :data => datastream)
+ end
+ end
+ end
+
+ # Returns an array of datastream ids which are present on this device
+ def datastream_ids
+ @datastream_ref.keys
+ end
+
+ def datastream_values
+ v = []
+ @datastream_ref.each do |id, ds|
+ v << "#{id} in #{ds.unit_label} (#{ds.unit_symbol}) = #{ds.current_value}"
+ end
+ v
+ end
+
+ def has_datastream?(channel)
+ @datastream_ref.has_key?(channel)
+ end
+
+ def has_channel?(channel)
+ has_datastream?(channel)
+ end
+
+ def is_private?
+ private == 'true'
+ end
+
+ def is_frozen?
+ status == 'frozen'
+ end
+
+ def has_location?
+ @has_location ||= false
+ end
+
+ def location
+ @location ||= OpenStruct.new(
+ :name => location_name,
+ :latitude => location_lat,
+ :longitude => location_lon,
+ :elevation => location_ele,
+ :exposure => location_exposure,
+ :disposition => location_disposition,
+ :waypoints => location_waypoints,
+ :domain => location_domain
+ )
+ end
+
+ end
+
+end
View
25 lib/xively-rb-connector/logging.rb
@@ -0,0 +1,25 @@
+require 'log4r'
+include Log4r
+module XivelyConnector
+
+ module Logging
+
+ def logger
+ @logger ||= XivelyConnector::Logging.logger
+ end
+
+ def self.logger
+ @logger ||= self.configure_logger_for(self.class.name)
+ end
+
+ def self.configure_logger_for(classname)
+ l = Logger.new(classname)
+ l.level = ERROR
+ l.trace = false
+ l.add Log4r::Outputter.stderr
+ l
+ end
+
+ end
+
+end
View
9 lib/xively-rb-connector/version.rb
@@ -0,0 +1,9 @@
+module XivelyConnector
+ module VERSION #:nodoc:
+ MAJOR = 0
+ MINOR = 1
+ PATCH = 0
+
+ STRING = [MAJOR, MINOR, PATCH].join('.')
+ end
+end
View
30 xively-rb-connector.gemspec
@@ -0,0 +1,30 @@
+# coding: utf-8
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require 'xively-rb-connector/version'
+
+Gem::Specification.new do |spec|
+ spec.name = "xively-rb-connector"
+ spec.version = XivelyConnector::VERSION::STRING
+ spec.authors = ["Jordan Duggan"]
+ spec.email = ["Jordan.Duggan@gmail.com"]
+ spec.description = %q{Ruby gem that provides an interface to Xively by extending xively-rb with convenience functions such Device.find_by_id, a datastream compression (only saves datapoints when value changes), a datapoint recording buffer, etc.}
+ spec.summary = %q{Ruby gem that provides an interface to Xively by extending xively-rb with convenience functions such Device.find_by_id, a datastream compression (only saves datapoints when value changes), a datapoint recording buffer, etc.}
+ spec.homepage = "https://github.com/jwtd/xively-rb-connector"
+ spec.license = "MIT"
+
+ spec.files = `git ls-files`.split($/)
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
+ spec.require_paths = ["lib"]
+
+ # Development dependencies
+ spec.add_development_dependency "bundler", "~> 1.3"
+ spec.add_development_dependency "rake"
+
+ # Runtime dependencies
+ spec.add_runtime_dependency "log4r"
+ spec.add_runtime_dependency "xively-rb"
+ spec.add_runtime_dependency "bigdecimal"
+
+end

0 comments on commit 06764f5

Please sign in to comment.