purple_air_api 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 01b62b7c6de5e4d8448ae7a1285db326b712464b3e9b18ed2d6ae828f6d1146c
4
+ data.tar.gz: 48f3649122ce37ec5413588bcdf8d12105506303f749e10989d89cb7cc2b69d0
5
+ SHA512:
6
+ metadata.gz: d5af049019b69727c5a0421abe9dc2dcfbec967532728caeb4fdc5cf29401d80b99c4381f7612bbac56f3beab33bf0fb1d108fa6539b2b81b39e55b0ae02f9d1
7
+ data.tar.gz: 845f907c45befc6d3abed0fae9e586ce0a05a818d0e7376cf4213a1152b15b6d4026a380a6da95825435a9e28dfd2134b616b4e4b831430959ac075a1a586276
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ .idea/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,15 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7.0
3
+ NewCops: enable
4
+
5
+ Metrics/BlockLength:
6
+ Exclude:
7
+ - '**/*_spec.rb'
8
+
9
+ Metrics:
10
+ Exclude:
11
+ - '**/raise_http_exception.rb'
12
+
13
+ Metrics/ClassLength:
14
+ Exclude:
15
+ - '**/get_sensors.rb'
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at dylankiselbach@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in purple_air_api.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ group :test, :development do
11
+ gem 'faker'
12
+ gem 'rspec', '~> 3.0'
13
+ gem 'rubocop', '~> 1.9'
14
+ gem 'webmock'
15
+ gem 'yard'
16
+ # for yard
17
+ gem 'webrick'
18
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,86 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ purple_air_api (0.1.0)
5
+ faraday (~> 1.3)
6
+ fast_jsonparser (~> 0.5)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.7.0)
12
+ public_suffix (>= 2.0.2, < 5.0)
13
+ ast (2.4.2)
14
+ concurrent-ruby (1.1.8)
15
+ crack (0.4.5)
16
+ rexml
17
+ diff-lcs (1.4.4)
18
+ faker (2.15.1)
19
+ i18n (>= 1.6, < 2)
20
+ faraday (1.3.0)
21
+ faraday-net_http (~> 1.0)
22
+ multipart-post (>= 1.2, < 3)
23
+ ruby2_keywords
24
+ faraday-net_http (1.0.1)
25
+ fast_jsonparser (0.5.0)
26
+ hashdiff (1.0.1)
27
+ i18n (1.8.7)
28
+ concurrent-ruby (~> 1.0)
29
+ multipart-post (2.1.1)
30
+ parallel (1.20.1)
31
+ parser (3.0.0.0)
32
+ ast (~> 2.4.1)
33
+ public_suffix (4.0.6)
34
+ rainbow (3.0.0)
35
+ rake (13.0.3)
36
+ regexp_parser (2.0.3)
37
+ rexml (3.2.4)
38
+ rspec (3.10.0)
39
+ rspec-core (~> 3.10.0)
40
+ rspec-expectations (~> 3.10.0)
41
+ rspec-mocks (~> 3.10.0)
42
+ rspec-core (3.10.1)
43
+ rspec-support (~> 3.10.0)
44
+ rspec-expectations (3.10.1)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.10.0)
47
+ rspec-mocks (3.10.1)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.10.0)
50
+ rspec-support (3.10.1)
51
+ rubocop (1.9.0)
52
+ parallel (~> 1.10)
53
+ parser (>= 3.0.0.0)
54
+ rainbow (>= 2.2.2, < 4.0)
55
+ regexp_parser (>= 1.8, < 3.0)
56
+ rexml
57
+ rubocop-ast (>= 1.2.0, < 2.0)
58
+ ruby-progressbar (~> 1.7)
59
+ unicode-display_width (>= 1.4.0, < 3.0)
60
+ rubocop-ast (1.4.1)
61
+ parser (>= 2.7.1.5)
62
+ ruby-progressbar (1.11.0)
63
+ ruby2_keywords (0.0.4)
64
+ unicode-display_width (2.0.0)
65
+ webmock (3.11.1)
66
+ addressable (>= 2.3.6)
67
+ crack (>= 0.3.2)
68
+ hashdiff (>= 0.4.0, < 2.0.0)
69
+ webrick (1.7.0)
70
+ yard (0.9.26)
71
+
72
+ PLATFORMS
73
+ x86_64-darwin-19
74
+
75
+ DEPENDENCIES
76
+ faker
77
+ purple_air_api!
78
+ rake (~> 13.0)
79
+ rspec (~> 3.0)
80
+ rubocop (~> 1.9)
81
+ webmock
82
+ webrick
83
+ yard
84
+
85
+ BUNDLED WITH
86
+ 2.2.5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 dkiselbach
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # PurpleAirApi
2
+
3
+ This is a client for interacting with the PurpleAir API. This was written using the V1 of the API. In order to use this gem, you must have been granted read and write tokens from PurpleAir.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'purple_air_api'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install purple_air_api
20
+
21
+ ## Usage
22
+
23
+ To use this gem, instantiate an instance of a PurpleAirApi client by making the following request:
24
+
25
+ `client = PurpleAirApi.client(read_token: your_read_token, write_token: your_write_token)`
26
+
27
+ You can then use this client to interact with the various API methods under the client like:
28
+
29
+ `client.get_sensors(options)`
30
+
31
+ Options would be and of the parameters you would like to pass onto the PurpleAir API. The gem will parse the parameters into the format required by the API.
32
+
33
+ ## Development
34
+
35
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
+
37
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
38
+
39
+ ## Contributing
40
+
41
+ Bug reports and pull requests are welcome on GitHub at https://github.com/dkiselbach/purple_air_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/dkiselbach/purple_air_api/CODE_OF_CONDUCT.md).
42
+
43
+ ## License
44
+
45
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
46
+
47
+ ## Code of Conduct
48
+
49
+ Everyone interacting in the PurpleAirApi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dkiselbach/purple_air_api/CODE_OF_CONDUCT.md).
50
+
51
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](code_of_conduct.md)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'purple_air_api'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ require 'irb'
11
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'fast_jsonparser'
5
+ require_relative 'purple_air_api/version'
6
+ require_relative 'purple_air_api/V1/client'
7
+ require_relative 'purple_air_api/V1/raise_http_exception'
8
+ require_relative 'purple_air_api/V1/errors'
9
+ require_relative 'purple_air_api/v1/sensors/get_sensors'
10
+ require_relative 'purple_air_api/v1/sensors/get_sensor'
11
+ require_relative 'purple_air_api/v1/sensors/errors'
12
+
13
+ # The PurpleAirApi is a gem intended to be used to interact with the PurpleAir API easily.
14
+ module PurpleAirApi
15
+ # Alias for PurpleAirApi::V1::Client.new
16
+ #
17
+ # @return [PurpleAirApi::V1::Client]
18
+ # @example requesting data for a few sensors
19
+ # options = { fields: ['icon', 'name'], location_type: ['outside'], show_only: [26, 41], max_age: 3600}
20
+ # PurpleAirApi.client(read_token: "1234", write_token: "1234").request_sensors(options)
21
+
22
+ def self.client(read_token:, write_token: nil)
23
+ PurpleAirApi::V1::Client.new(read_token: read_token, write_token: write_token)
24
+ end
25
+
26
+ # Delegate to PurpleAirApi::V1::Client
27
+ def self.method_missing(method, *args, &block)
28
+ return super unless client.respond_to?(method)
29
+
30
+ client.send(method, *args, &block)
31
+ end
32
+
33
+ # Delegate to PurpleAirApi::V1::Client
34
+ def self.respond_to?(method, include_all: false)
35
+ client.respond_to?(method, include_all) || super
36
+ end
37
+
38
+ # Delegate to PurpleAirApi::V1::Client
39
+ def self.respond_to_missing?(method_name, include_private: false)
40
+ client.respond_to_missing?(method_name, include_private) || super
41
+ end
42
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurpleAirApi
4
+ # The V1 API Module for namespacing the PurpleAir V1 API
5
+ module V1
6
+ # Client class for interfacing with the V1 PurpleAir API. Refer to the instance methods to learn more about what
7
+ # methods and API options are available.
8
+ class Client
9
+ attr_reader :read_client, :write_client
10
+
11
+ # The base URL for the PurpleAir API
12
+ API_URL = 'https://api.purpleair.com/v1/'
13
+
14
+ # Creates a read and write client to interface with the Purple Air API.
15
+ # @!method initialize(read_token:, write_token:)
16
+ # @param read_token [String] The read client you received from PurpleAir
17
+ # @param write_token [String] The write client you received from PurpleAir
18
+ # @example generate a client instance
19
+ # PurpleAirApi::V1::Client.new(read_token: "1234", write_token: "1234")
20
+
21
+ def initialize(read_token:, write_token: nil)
22
+ @read_client = create_http_client(read_token)
23
+ @write_client = create_http_client(write_token) unless write_token.nil?
24
+ end
25
+
26
+ # Makes a request to the sensors endpoint and returns an instance of the V1::GetSensors class.
27
+ # @!method request_sensors(options)
28
+ # @param [Hash] options A hash of options { :option_name=>value }
29
+ # @option options [Array<Integer>] :show_only An array of indexes for the sensors you want to request.
30
+ # @option options [Array<String>] :location_type An array of strings specifying sensor location.
31
+ # @option options [Array<String>] :fields An array of fields you want returned.
32
+ # @option options [Integer] :modified_since Only return sensors updated since this timestamp.
33
+ # @option options [Integer] :max_age Only return sensors updated in the last n seconds.
34
+ # @option options [Hash<array>] :bounding_box A hash with a :nw and :se array { nw: [lat,long], se: [lat,long] }.
35
+ # @example
36
+ # { bounding_box: { nw: [37.7790262, -122.4199061], se: [37.6535403, -122.4168664]}}
37
+ # @option options [Array<String>] :read_keys An array of read-keys which are required for private devices.
38
+ # @return [PurpleAirApi::GetSensors]
39
+ # @example request sensor data for a few sensors
40
+ # options = { fields: ['icon', 'name'], location_type: ['outside'], show_only: [20, 47], max_age: 3600}
41
+ # client = PurpleAirApi::V1::Client.new(read_token: "1234", write_token: "1234")
42
+ # response = client.request_sensors(options)
43
+ # response_hash = response.parsed_response
44
+
45
+ def request_sensors(options = {})
46
+ GetSensors.call(client: read_client, **options)
47
+ end
48
+
49
+ def request_sensor(sensor_index:, read_key: nil)
50
+ GetSensor.call(client: read_client, sensor_index: sensor_index, read_key: read_key)
51
+ end
52
+
53
+ private
54
+
55
+ def create_http_client(token)
56
+ Faraday.new(url: API_URL) do |faraday|
57
+ faraday.headers['X-API-KEY'] = token
58
+ faraday.use RaiseHttpException
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurpleAirApi
4
+ module V1
5
+ # A custom error class for rescuing from all PurpleAir API errors
6
+ class BaseError < StandardError
7
+ attr_reader :response_object, :error_type
8
+
9
+ # Initialize the error object with error_type and the Faraday response object. PurpleAir returns a human friendly
10
+ # error message and type which is added here. You can also reference the response to view the raw response
11
+ # from PurpleAir.
12
+ # @!method initialize(message, error_type, response_object)
13
+ # @param message [String] the message you want displayed when the error is raised
14
+ # @param error_type [String] the error type that PurpleAir includes in the JSON response
15
+ # @param response_object [Faraday::Env] the Faraday response object
16
+ def initialize(message, error_type, response_object)
17
+ super(message)
18
+ @error_type = error_type
19
+ @response_object = response_object
20
+ end
21
+ end
22
+
23
+ # Raised when the PurpleAir API returns the HTTP status code 403
24
+ class ApiKeyError < BaseError
25
+ end
26
+
27
+ # Raised when the PurpleAir API returns the HTTP status code 415
28
+ class MissingJsonPayloadError < BaseError
29
+ end
30
+
31
+ # Raised when the PurpleAir API returns the HTTP status code 400. Use the message and error_type on the Error
32
+ # object to determine additional information regarding the error.
33
+ class ApiError < BaseError
34
+ end
35
+
36
+ # Raised when the PurpleAir API returns the HTTP status code 404
37
+ class NotFoundError < BaseError
38
+ end
39
+
40
+ # Raised when the PurpleAir API returns the HTTP status code 500
41
+ class ServerError < BaseError
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurpleAirApi
4
+ module V1
5
+ # Handles any HTTP exceptions for 400 and 500 error codes
6
+ class RaiseHttpException < Faraday::Middleware
7
+ # A switch statement which determines which error to raise depending on error code
8
+ def call(env)
9
+ @app.call(env).on_complete do |response|
10
+ self.response = response
11
+ case response[:status].to_i
12
+ when 400
13
+ raise ApiError.new(error_message, parsed_response[:error], response)
14
+ when 403
15
+ raise ApiKeyError.new(error_message, parsed_response[:error], response)
16
+ when 404
17
+ raise NotFoundError.new(error_message, parsed_response[:error], response)
18
+ when 415
19
+ raise MissingJsonPayloadError.new(error_message, 'MissingJsonPayloadError', response)
20
+ when 500
21
+ raise ServerError.new(error_message, 'ServerError', response)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Faraday syntax for implementing middleware
27
+ def initialize(app)
28
+ super app
29
+ @parser = nil
30
+ end
31
+
32
+ private
33
+
34
+ attr_accessor :response
35
+
36
+ def error_message
37
+ parsed_response[:description]
38
+ end
39
+
40
+ def parsed_response
41
+ @parsed_response ||= FastJsonparser.parse(response[:body])
42
+ rescue FastJsonparser::ParseError
43
+ unknown_error_message
44
+ end
45
+
46
+ def unknown_error_message
47
+ { description: 'Something went wrong in the request.' }
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurpleAirApi
4
+ module V1
5
+ # Raised when the options given are invalid
6
+ class OptionsError < StandardError
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurpleAirApi
4
+ module V1
5
+ # Class for requesting sensor data for a single sensor. This will return different response formats from the API.
6
+ class GetSensor
7
+ attr_accessor :http_response, :request_options, :index
8
+ attr_reader :http_client
9
+
10
+ # The endpoint URL
11
+ URL = 'https://api.purpleair.com/v1/sensors/'
12
+
13
+ # Calls initializes the class and requests the data from PurpleAir.
14
+ # @!method call(...)
15
+
16
+ def self.call(...)
17
+ new(...).request
18
+ end
19
+
20
+ # Creates a HTTP friendly options hash depending on your inputs
21
+ # @!method initialize(client:, **options)
22
+ # @param client [Faraday::Connection] Your HTTP client initialized in Client
23
+ # @param options [Hash] Your HTTP options for the request.
24
+
25
+ def initialize(client:, sensor_index:, read_key: nil)
26
+ @http_client = client
27
+ @request_options = {}
28
+ create_options_hash(sensor_index, read_key)
29
+ end
30
+
31
+ # Makes a get request to the PurpleAir Get Sensors Data endpoint https://api.purpleair.com/v1/sensors.
32
+ # @!method request
33
+ # @return [PurpleAirApi::V1::GetSensors]
34
+
35
+ def request
36
+ self.http_response = http_client.get(url, request_options)
37
+ self
38
+ end
39
+
40
+ # Delegate to PurpleAirApi::V1::GetSensor.json_response
41
+
42
+ def parsed_response
43
+ json_response
44
+ end
45
+
46
+ # Takes the raw response from PurpleAir and parses the JSON.
47
+ # @!method json_response
48
+ # @return [Json]
49
+ # @example json_response example
50
+ # response.json_response
51
+ # {
52
+ # "api_version": "V1.0.6-0.0.9",
53
+ # "time_stamp": 1615053213,
54
+ # "sensor": {
55
+ # "sensor_index": 20,
56
+ # "name": "Oakdale",
57
+ # "model": "UNKNOWN",
58
+ # "location_type": 0,
59
+ # "latitude": 40.6031,
60
+ # "longitude": -111.8361,
61
+ # "altitude": 4636,
62
+ # "last_seen": 1615053181,
63
+ # "last_modified": 1575003022,
64
+ # "private": 0,
65
+ # "channel_state": 1,
66
+ # "channel_flags_manual": 2,
67
+ # "pm1.0_a": 0.0,
68
+ # "pm2.5_a": 0.0,
69
+ # "pm10.0_a": 0.0,
70
+ # "0.3_um_count_a": 0.0,
71
+ # "0.5_um_count_a": 0.0,
72
+ # "1.0_um_count_a": 0.0,
73
+ # "2.5_um_count_a": 0.0,
74
+ # "5.0_um_count_a": 0.0,
75
+ # "10.0_um_count_a": 0.0,
76
+ # "stats_a": {
77
+ # "pm2.5": 0.0,
78
+ # "pm2.5_10minute": 0.0,
79
+ # "pm2.5_30minute": 0.0,
80
+ # "pm2.5_60minute": 0.0,
81
+ # "pm2.5_6hour": 0.0,
82
+ # "pm2.5_24hour": 0.0,
83
+ # "pm2.5_1week": 0.0,
84
+ # "time_stamp": 1615053181
85
+ # },
86
+ # "analog_input": 0.01,
87
+ # "primary_id_a": 66984,
88
+ # "primary_key_a": "TLKXL2SOJ9M0KGFK",
89
+ # "secondary_id_a": 71207,
90
+ # "secondary_key_a": "224YRSOGD8TNE5FQ",
91
+ # "primary_id_b": 209227,
92
+ # "primary_key_b": "3YF75LZ4DL7HDH9O",
93
+ # "secondary_id_b": 209228,
94
+ # "secondary_key_b": "Y02CNZZDMR15KCMX",
95
+ # "hardware": "1.0+PMSX003-O",
96
+ # "led_brightness": 0.0,
97
+ # "firmware_version": "6.01",
98
+ # "rssi": -78.0,
99
+ # "icon": 0,
100
+ # "channel_flags_auto": 0
101
+ # }
102
+ # }
103
+
104
+ def json_response
105
+ @json_response ||= FastJsonparser.parse(http_response.body)
106
+ end
107
+
108
+ private
109
+
110
+ def create_options_hash(sensor_index, read_key)
111
+ sensor_index(sensor_index)
112
+ request_options.merge!(
113
+ read_key(read_key)
114
+ )
115
+ end
116
+
117
+ def sensor_index(index)
118
+ raise OptionsError, 'sensor_index must be an Integer' unless index.instance_of?(Integer)
119
+
120
+ self.index = index.to_s
121
+ end
122
+
123
+ def read_key(key)
124
+ return {} if key.nil?
125
+
126
+ raise OptionsError, 'read_key must be a String' unless key.instance_of?(String)
127
+
128
+ { read_key: key }
129
+ end
130
+
131
+ def url
132
+ URL + index
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurpleAirApi
4
+ module V1
5
+ # Class for requesting sensor data. This will return different response formats from the API.
6
+ class GetSensors
7
+ attr_accessor :request_options, :http_response
8
+ attr_reader :http_client
9
+ attr_writer :parsed_response
10
+
11
+ # The default value for fields that will be returned by PurpleAir
12
+ DEFAULT_FIELDS = %w[icon name latitude longitude altitude pm2.5].freeze
13
+
14
+ # The default location type for the sensor
15
+ DEFAULT_LOCATION_TYPE = %w[outside inside].freeze
16
+
17
+ # The endpoint URL
18
+ URL = 'https://api.purpleair.com/v1/sensors'
19
+
20
+ # Calls initializes the class and requests the data from PurpleAir.
21
+ # @!method call(...)
22
+
23
+ def self.call(...)
24
+ new(...).request
25
+ end
26
+
27
+ # Creates a HTTP friendly options hash depending on your inputs
28
+ # @!method initialize(client:, **options)
29
+ # @param client [Faraday::Connection] Your HTTP client initialized in Client
30
+ # @param options [Hash] Your HTTP options for the request.
31
+
32
+ def initialize(client:, **options)
33
+ @http_client = client
34
+ @request_options = {}
35
+ create_options_hash(options)
36
+ end
37
+
38
+ # Makes a get request to the PurpleAir Get Sensors Data endpoint https://api.purpleair.com/v1/sensors.
39
+ # @!method request
40
+ # @return [PurpleAirApi::V1::GetSensors]
41
+
42
+ def request
43
+ self.http_response = http_client.get(URL, request_options)
44
+ self
45
+ end
46
+
47
+ # Takes the raw response from PurpleAir and generates a hash indexed by sensor index. You can use this response
48
+ # like a normal hash object. This GetSensorsClass.parsed_response[:47] would return a hash of data for sensor 47
49
+ # with each hash key labelling the associated data.
50
+ # @!method parsed_response
51
+ # @return [Hash]
52
+ # @example parsed_response example
53
+ # response.parsed_response
54
+ # {
55
+ # :fields=>["sensor_index", "name", "icon", "latitude", "longitude", "altitude", "pm2.5"],
56
+ # :api_version=>"V1.0.6-0.0.9",
57
+ # :time_stamp=>1614787814,
58
+ # :data_time_stamp=>nil,
59
+ # :max_age=>3600,
60
+ # :data=>
61
+ # {
62
+ # 20=>{"sensor_index"=>20, "name"=>"Oakdale", "icon"=>0, "latitude"=>40.6031,
63
+ # "longitude"=>-111.8361, "altitude"=>4636, "pm2.5"=>0.0},
64
+ # 47=>{"sensor_index"=>47, "name"=>"OZONE TEST", "icon"=>0, "latitude"=>40.4762,
65
+ # "longitude"=>-111.8826, "altitude"=>nil, "pm2.5"=>nil}
66
+ # }
67
+ # }
68
+
69
+ def parsed_response
70
+ @parsed_response ||= parse_response
71
+ end
72
+
73
+ # Takes the raw response from PurpleAir and parses the JSON.
74
+ # @!method json_response
75
+ # @return [Json]
76
+ # @example json_response example
77
+ # response.json_response
78
+ # {
79
+ # :api_version=>"V1.0.6-0.0.9",
80
+ # :time_stamp=>1614787814,
81
+ # :data_time_stamp=>1614787807,
82
+ # :location_type=>0,
83
+ # :max_age=>3600,
84
+ # :fields=>["sensor_index", "name", "icon", "latitude", "longitude", "altitude", "pm2.5"],
85
+ # :data=>[
86
+ # [20, "Oakdale", 0, 40.6031, -111.8361, 4636, 0.0],
87
+ # [47, "OZONE TEST", 0, 40.4762, -111.8826, nil, nil]
88
+ # ]
89
+ # }
90
+
91
+ def json_response
92
+ @json_response ||= FastJsonparser.parse(http_response.body)
93
+ end
94
+
95
+ private
96
+
97
+ attr_accessor :data, :data_fields
98
+
99
+ def create_options_hash(options)
100
+ request_options.merge!(
101
+ fields(options[:fields] || DEFAULT_FIELDS),
102
+ location_type(options[:location_type]),
103
+ show_only(options[:show_only]),
104
+ modified_since(options[:modified_since]),
105
+ max_age(options[:max_age]),
106
+ bounding_box(options[:bounding_box]),
107
+ read_keys(options[:read_keys])
108
+ )
109
+ end
110
+
111
+ def fields(fields_array)
112
+ unless fields_array.instance_of?(Array) && fields_array.first.instance_of?(String)
113
+ raise OptionsError, 'fields must be an array of strings specifying the fields you want returned'
114
+ end
115
+
116
+ { fields: fields_array.join(',') }
117
+ end
118
+
119
+ def show_only(sensor_indices)
120
+ return {} if sensor_indices.nil?
121
+
122
+ unless sensor_indices.instance_of?(Array) && sensor_indices.first.instance_of?(Integer)
123
+ raise OptionsError,
124
+ 'show_only must be an array of integers specifying the sensor indices you data returned for'
125
+ end
126
+
127
+ { show_only: sensor_indices.join(',') }
128
+ end
129
+
130
+ def location_type(location_type)
131
+ return {} if location_type.nil?
132
+
133
+ unless location_type.instance_of?(Array) && location_type.first.instance_of?(String)
134
+ raise OptionsError,
135
+ "location_type must be an array of strings specifying either ['inside'], ['outside'], or both"
136
+ end
137
+
138
+ location_type.map!(&:downcase)
139
+
140
+ location_type_parameter(location_type)
141
+ end
142
+
143
+ def location_type_parameter(location_type)
144
+ return {} if location_type.include?('outside') && location_type.include?('inside')
145
+ return { location_type: 1 } if location_type.include?('inside')
146
+ return { location_type: 0 } if location_type.include?('outside')
147
+ end
148
+
149
+ def modified_since(timestamp)
150
+ return {} if timestamp.nil?
151
+ raise OptionsError, 'timestamp must be a valid Integer' unless timestamp.instance_of?(Integer)
152
+
153
+ { modified_since: timestamp }
154
+ end
155
+
156
+ def max_age(seconds)
157
+ return {} if seconds.nil?
158
+ raise OptionsError, 'seconds must be a valid Integer' unless seconds.instance_of?(Integer)
159
+
160
+ { max_age: seconds.to_i }
161
+ end
162
+
163
+ def bounding_box(coordinates)
164
+ return {} if coordinates.nil?
165
+
166
+ unless coordinates.instance_of?(Hash) && coordinates[:nw].length == 2 && coordinates[:se].length == 2
167
+ raise OptionsError, 'coordinates must be a Hash with a :nw and :se array containing [lat, long]'
168
+ end
169
+
170
+ {
171
+ nwlat: coordinates[:nw][0], nwlng: coordinates[:nw][1], selat: coordinates[:se][0], selng: coordinates[:se][1]
172
+ }
173
+ end
174
+
175
+ def read_keys(keys)
176
+ return {} if keys.nil?
177
+
178
+ unless keys.instance_of?(Array) && keys.first.instance_of?(String)
179
+ raise OptionsError, 'read_keys must be an Array of Strings'
180
+ end
181
+
182
+ { read_keys: keys.join(',') }
183
+ end
184
+
185
+ def parse_response
186
+ response = json_response
187
+
188
+ generate_response_hash(response)
189
+
190
+ self.data = response[:data]
191
+ self.data_fields = response[:fields]
192
+
193
+ merge_data_hash
194
+ end
195
+
196
+ def generate_response_hash(response_hash)
197
+ fields, api_version,
198
+ time_stamp, date_time_stamp, max_age = response_hash.values_at(:fields, :api_version, :time_stamp,
199
+ :date_time_stamp, :max_age)
200
+ self.parsed_response = {
201
+ fields: fields,
202
+ api_version: api_version,
203
+ time_stamp: time_stamp,
204
+ data_time_stamp: date_time_stamp,
205
+ max_age: max_age
206
+ }
207
+ end
208
+
209
+ def merge_data_hash
210
+ parsed_response.merge!({ data: generate_data_hash })
211
+ end
212
+
213
+ def generate_data_hash
214
+ data_hash = {}
215
+ data.each do |sensor|
216
+ sensor_index = sensor.first
217
+ data_hash.merge!(sensor_index => {})
218
+ sensor.each_with_index do |data_point, index|
219
+ data_hash[sensor_index].merge!(data_fields[index] => data_point)
220
+ end
221
+ end
222
+ data_hash
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PurpleAirApi
4
+ # The gem version
5
+ VERSION = '0.1.0'
6
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/purple_air_api/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'purple_air_api'
7
+ spec.version = PurpleAirApi::VERSION
8
+ spec.authors = ['Dylan Kiselbach']
9
+ spec.email = ['dylankiselbach@gmail.com']
10
+
11
+ spec.summary = 'This is an library to assist in interacting with the PurpleAir API.'
12
+ spec.description = 'To use this gem you will need your own read and write key from PurpleAir which can be retrieved
13
+ by contacting their support team https://www2.purpleair.com/pages/contact-us.'
14
+ spec.homepage = 'https://github.com/dkiselbach/purple_air_api'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/dkiselbach/purple_air_api.'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/dkiselbach/purple_air_api.'
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
26
+ end
27
+ spec.bindir = 'exe'
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ['lib']
30
+
31
+ # Uncomment to register a new dependency of your gem
32
+ spec.add_dependency 'faraday', '~> 1.3'
33
+ spec.add_dependency 'fast_jsonparser', '~> 0.5'
34
+
35
+ # For more information and examples about making a new gem, checkout our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: purple_air_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Dylan Kiselbach
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-03-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fast_jsonparser
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.5'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.5'
41
+ description: |-
42
+ To use this gem you will need your own read and write key from PurpleAir which can be retrieved
43
+ by contacting their support team https://www2.purpleair.com/pages/contact-us.
44
+ email:
45
+ - dylankiselbach@gmail.com
46
+ executables: []
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".gitignore"
51
+ - ".rspec"
52
+ - ".rubocop.yml"
53
+ - CODE_OF_CONDUCT.md
54
+ - Gemfile
55
+ - Gemfile.lock
56
+ - LICENSE.txt
57
+ - README.md
58
+ - Rakefile
59
+ - bin/console
60
+ - bin/setup
61
+ - lib/purple_air_api.rb
62
+ - lib/purple_air_api/V1/client.rb
63
+ - lib/purple_air_api/V1/errors.rb
64
+ - lib/purple_air_api/V1/raise_http_exception.rb
65
+ - lib/purple_air_api/V1/sensors/errors.rb
66
+ - lib/purple_air_api/V1/sensors/get_sensor.rb
67
+ - lib/purple_air_api/V1/sensors/get_sensors.rb
68
+ - lib/purple_air_api/version.rb
69
+ - purple_air_api.gemspec
70
+ homepage: https://github.com/dkiselbach/purple_air_api
71
+ licenses:
72
+ - MIT
73
+ metadata:
74
+ homepage_uri: https://github.com/dkiselbach/purple_air_api
75
+ source_code_uri: https://github.com/dkiselbach/purple_air_api.
76
+ changelog_uri: https://github.com/dkiselbach/purple_air_api.
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.7.0
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.2.3
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: This is an library to assist in interacting with the PurpleAir API.
96
+ test_files: []