weatherlink 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d16465c0ef961c536b4a160c17993ae39978a833ff0602bb3c2975dcdb12225b
4
+ data.tar.gz: e33b3cd29d2430334633ca59020bca24f18cb13d1b0ff2ab5c1e650dfd1f0436
5
+ SHA512:
6
+ metadata.gz: 651ba70b2b30dac3401707ba115aca784be784da3e5f7519b4d630fbe9f22e97e7f6ee2a706e0de248e1efea96e0c330e0df4f4a8642b5072a3e2dadfbd0bec2
7
+ data.tar.gz: b651ba906113a68977be4cda967dddcca66ca556a481b4d6bbe66723d115b157f161509d9cb0a759e02dce84a11aa45f8869aac77a6c52500a1f1d0d53ffa2be
@@ -0,0 +1,18 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor/
10
+
11
+ *.gem
12
+
13
+ # rspec failure tracking
14
+ .rspec_status
15
+
16
+ # RubyMine files
17
+ .idea
18
+ .rakeTasks
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,27 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ NewCops: enable
4
+ Layout/LineLength:
5
+ Max: 120
6
+ Style/TrailingCommaInArrayLiteral:
7
+ EnforcedStyleForMultiline: consistent_comma
8
+ Style/TrailingCommaInHashLiteral:
9
+ EnforcedStyleForMultiline: consistent_comma
10
+ Style/FormatString:
11
+ Enabled: false
12
+ Style/FormatStringToken:
13
+ Enabled: false
14
+ Style/Documentation:
15
+ Enabled: false
16
+ Metrics/ClassLength:
17
+ Enabled: false
18
+ Metrics/MethodLength:
19
+ Enabled: false
20
+ Metrics/AbcSize:
21
+ Enabled: false
22
+ Metrics/CyclomaticComplexity:
23
+ Enabled: false
24
+ Metrics/PerceivedComplexity:
25
+ Enabled: false
26
+ Style/SymbolArray:
27
+ MinSize: 1
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.6.6
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in weatherlink.gemspec
6
+ gemspec
@@ -0,0 +1,36 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ weatherlink (0.1.0)
5
+ ruby-units (~> 2.3)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ diff-lcs (1.3)
11
+ rake (12.3.3)
12
+ rspec (3.9.0)
13
+ rspec-core (~> 3.9.0)
14
+ rspec-expectations (~> 3.9.0)
15
+ rspec-mocks (~> 3.9.0)
16
+ rspec-core (3.9.2)
17
+ rspec-support (~> 3.9.3)
18
+ rspec-expectations (3.9.2)
19
+ diff-lcs (>= 1.2.0, < 2.0)
20
+ rspec-support (~> 3.9.0)
21
+ rspec-mocks (3.9.1)
22
+ diff-lcs (>= 1.2.0, < 2.0)
23
+ rspec-support (~> 3.9.0)
24
+ rspec-support (3.9.3)
25
+ ruby-units (2.3.1)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ rake (~> 12.0)
32
+ rspec (~> 3.0)
33
+ weatherlink!
34
+
35
+ BUNDLED WITH
36
+ 2.1.4
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Jeremy Cole
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.
@@ -0,0 +1,177 @@
1
+ # WeatherLink
2
+
3
+ This is an unofficial implementation of the Davis Instruments WeatherLink API, including both the Local API (v1) and the web API (v2).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'weatherlink'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install weatherlink
20
+
21
+ ## Usage
22
+
23
+ ### Local API
24
+
25
+ To read the data from a local WeatherLink Live device via the "local API" (there is no authentication):
26
+
27
+ ```
28
+ require 'weatherlink'
29
+
30
+ > wl = WeatherLink::LocalClient.new(host: '<local ip address>')
31
+ ```
32
+
33
+ The `current_conditions` method returns a `SensorData` object for each sensors:
34
+
35
+ ```
36
+ > cc = wl.current_conditions
37
+ => #<WeatherLink::SensorDataCollection (3 sensors)>
38
+
39
+ > cc.to_a
40
+ => [#<WeatherLink::SensorData lsid=1 (Local API - ISS Record, 1 records)>,
41
+ #<WeatherLink::SensorData lsid=2 (Local API - LSS Temperature/Humidity Record, 1 records)>,
42
+ #<WeatherLink::SensorData lsid=3 (Local API - LSS Barometric Pressure Record, 1 records)>]
43
+ ```
44
+
45
+ Each `SensorData` is a wrapper for a hash containing the underlying data. Each of its keys can also be used as a method to get the data itself.
46
+
47
+ ```
48
+ > pp cc[0].to_h
49
+ {"ts"=>1609130694,
50
+ "temp"=>24.2 tempF,
51
+ "hum"=>84.9 %,
52
+ "dew_point"=>20.3 tempF,
53
+ "wet_bulb"=>22.6 tempF,
54
+ "heat_index"=>24.2 tempF,
55
+ "wind_chill"=>24.2 tempF,
56
+ "thw_index"=>24.2 tempF,
57
+ "thsw_index"=>22.2 tempF,
58
+ "wind_speed_last"=>0 mph,
59
+ "wind_dir_last"=>0 deg,
60
+ "wind_speed_avg_last_1_min"=>0 mph,
61
+ "wind_dir_scalar_avg_last_1_min"=>149 deg,
62
+ "wind_speed_avg_last_2_min"=>0 mph,
63
+ "wind_dir_scalar_avg_last_2_min"=>149 deg,
64
+ "wind_speed_hi_last_2_min"=>1 mph,
65
+ "wind_dir_at_hi_speed_last_2_min"=>149 deg,
66
+ "wind_speed_avg_last_10_min"=>0 mph,
67
+ "wind_dir_scalar_avg_last_10_min"=>149 deg,
68
+ "wind_speed_hi_last_10_min"=>1 mph,
69
+ "wind_dir_at_hi_speed_last_10_min"=>149 deg,
70
+ "rain_size"=>1,
71
+ "rain_rate_last"=>0 in/h,
72
+ "rain_rate_hi"=>0 in/h,
73
+ "rainfall_last_15_min"=>0 in,
74
+ "rain_rate_hi_last_15_min"=>0 in/h,
75
+ "rainfall_last_60_min"=>0 in,
76
+ "rainfall_last_24_hr"=>0 in,
77
+ "rain_storm"=>0 in,
78
+ "rain_storm_start_at"=>nil,
79
+ "solar_rad"=>0 W/m^2,
80
+ "uv_index"=>0.0,
81
+ "rx_state"=>0,
82
+ "trans_battery_flag"=>0,
83
+ "rainfall_daily"=>0 in,
84
+ "rainfall_monthly"=>18 in,
85
+ "rainfall_year"=>128 in,
86
+ "rain_storm_last"=>5 in,
87
+ "rain_storm_last_start_at"=>1608224100,
88
+ "rain_storm_last_end_at"=>1608318061}
89
+ ```
90
+
91
+ Note that the unit support uses `ruby-units` which uses `Rational`, so some of the results can be... interesting:
92
+
93
+ ```
94
+ > cc[0].temp
95
+ => -3342515348439043/791648371998720 tempC
96
+ ```
97
+
98
+ Use `scalar.to_f` to get a `Float` most of the time:
99
+
100
+ ```
101
+ > cc[0].temp.scalar.to_f
102
+ => -4.222222222222226
103
+ ```
104
+
105
+ ### Web API
106
+
107
+ Obtain API credentials form your WeatherLink account and initialize the API:
108
+
109
+ ```
110
+ require 'weatherlink'
111
+
112
+ > wl = WeatherLink::Client.new(api_key: '<api key>', api_secret: '<api secret>')
113
+ ```
114
+
115
+ Various aspects of the API and devices can be interrogated:
116
+
117
+ ```
118
+ > wl.stations
119
+ => [#<WeatherLink::Station station_id=1 gateway_id_hex=a (Jackalope)>]
120
+
121
+ > wl.sensors
122
+ => [#<WeatherLink::Sensor lsid=1 (Davis Instruments - WeatherLink LIVE Health)>,
123
+ #<WeatherLink::Sensor lsid=2 (Davis Instruments - Barometer)>,
124
+ #<WeatherLink::Sensor lsid=3 (Davis Instruments - Inside Temp/Hum)>,
125
+ #<WeatherLink::Sensor lsid=4 (Davis Instruments - Vantage Pro2 Plus /w 24-hr-Fan-Aspirated Radiation shield, UV & Solar Radiation Sensors)>,
126
+ #<WeatherLink::Sensor lsid=5 (Davis Instruments - AQS Health)>,
127
+ #<WeatherLink::Sensor lsid=6 (Davis Instruments - AirLink)>,
128
+ #<WeatherLink::Sensor lsid=7 (Davis Instruments - AQS Health)>,
129
+ #<WeatherLink::Sensor lsid=8 (Davis Instruments - AirLink)>,
130
+ #<WeatherLink::Sensor lsid=9 (Davis Instruments - AQS Health)>,
131
+ #<WeatherLink::Sensor lsid=10 (Davis Instruments - AirLink)>]
132
+
133
+ > wl.nodes
134
+ => [#<WeatherLink::Node device_id_hex=a (Station - Barn)>,
135
+ #<WeatherLink::Node device_id_hex=b (Station - Living Room)>,
136
+ #<WeatherLink::Node device_id_hex=c (Station - Office)>]
137
+ ```
138
+
139
+ Since most users will probably only have one station, a `station` method returns the first station for convenience:
140
+
141
+ ```
142
+ wl.station
143
+ => #<WeatherLink::Station station_id=1 gateway_id_hex=a (Station)>
144
+ ```
145
+
146
+ To collect current weather data from all sensor of the first/primary station:
147
+
148
+ ```
149
+ > wl.station.current
150
+ => #<WeatherLink::SensorDataCollection (10 sensors)>
151
+ ```
152
+
153
+ At this point accessing the underlying data is exactly the same as using the Local API above.
154
+
155
+ Since the station knows the IP addresses of each sensor, if you're on the local network you can also access these sensors directly through the `local_sensors` method (which uses the same `WeatherLink::LocalClient` API above, but automatically configures it for each known sensor):
156
+
157
+ ```
158
+ > wl.station.local_sensors
159
+ => [#<struct WeatherLink::Station::LocalSensor device=#<WeatherLink::Station station_id=1 gateway_id_hex=a (Station)>, host="1.2.3.4">,
160
+ #<struct WeatherLink::Station::LocalSensor device=#<WeatherLink::Node device_id_hex=b (Station - Living Room)>, host="1.2.3.5">,
161
+ #<struct WeatherLink::Station::LocalSensor device=#<WeatherLink::Node device_id_hex=c (Station - Office)>, host="1.2.3.6">,
162
+ #<struct WeatherLink::Station::LocalSensor device=#<WeatherLink::Node device_id_hex=d (Station - Barn)>, host="1.2.3.7">]
163
+ ```
164
+
165
+ ## Development
166
+
167
+ 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.
168
+
169
+ 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 tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
170
+
171
+ ## Contributing
172
+
173
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jeremycole/weatherlink.
174
+
175
+ ## License
176
+
177
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -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
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'weatherlink'
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
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -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,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'weatherlink/version'
4
+ require 'ruby-units'
5
+
6
+ module WeatherLink
7
+ class Error < StandardError; end
8
+
9
+ UNIT_TYPES = %i[
10
+ temperature
11
+ humidity
12
+ wind_speed
13
+ pressure
14
+ wind_direction
15
+ rain_quantity
16
+ rain_rate
17
+ solar_radiation
18
+ ].freeze
19
+
20
+ Units = Struct.new(*UNIT_TYPES, keyword_init: true) do
21
+ def fetch(key)
22
+ return send(key.to_sym) if key && respond_to?(key.to_sym)
23
+
24
+ nil
25
+ end
26
+ end
27
+
28
+ METRIC_WEATHER_UNITS = Units.new(
29
+ temperature: 'tempC',
30
+ humidity: '%',
31
+ pressure: 'hPa',
32
+ wind_speed: 'm/s',
33
+ wind_direction: 'deg',
34
+ rain_quantity: 'cm',
35
+ rain_rate: 'cm/h',
36
+ solar_radiation: 'W/m^2'
37
+ )
38
+
39
+ IMPERIAL_WEATHER_UNITS = Units.new(
40
+ temperature: 'tempF',
41
+ humidity: '%',
42
+ pressure: 'inHg',
43
+ wind_speed: 'mph',
44
+ wind_direction: 'deg',
45
+ rain_quantity: 'in',
46
+ rain_rate: 'in/h',
47
+ solar_radiation: 'W/m^2'
48
+ )
49
+
50
+ SystemType = Struct.new(:name, keyword_init: true)
51
+
52
+ RecordType = Struct.new(:id, :system, :name, :type, keyword_init: true) do
53
+ def description
54
+ "#{system.name} - #{name}"
55
+ end
56
+
57
+ def current_conditions?
58
+ type == :current_conditions
59
+ end
60
+
61
+ def archive?
62
+ type == :archive
63
+ end
64
+
65
+ def health?
66
+ type == :health
67
+ end
68
+ end
69
+ end
70
+
71
+ require 'weatherlink/hash_wrapper'
72
+
73
+ require 'weatherlink/api_v2'
74
+ require 'weatherlink/client'
75
+
76
+ require 'weatherlink/local_api_v1'
77
+ require 'weatherlink/local_client'
78
+
79
+ require 'weatherlink/data_record'
80
+ require 'weatherlink/sensor_data'
81
+ require 'weatherlink/sensor_record'
82
+ require 'weatherlink/sensor_data_collection'
83
+
84
+ require 'weatherlink/station'
85
+ require 'weatherlink/node'
86
+ require 'weatherlink/sensor'
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'json'
6
+
7
+ module WeatherLink
8
+ class APIv2
9
+ BASE_URI = 'https://api.weatherlink.com/v2'
10
+
11
+ SYSTEM_TYPES = {
12
+ Legacy: SystemType.new(name: 'Legacy'),
13
+ EnviroMonitor: SystemType.new(name: 'EnviroMonitor'),
14
+ WeatherLinkLive: SystemType.new(name: 'WeatherLink Live'),
15
+ AirLink: SystemType.new(name: 'AirLink'),
16
+ }.freeze
17
+
18
+ RECORD_TYPES = [
19
+ RecordType.new(
20
+ id: 1,
21
+ system: SYSTEM_TYPES[:Legacy],
22
+ name: 'Current Conditions Record - Revision A',
23
+ type: :current_conditions
24
+ ),
25
+ RecordType.new(
26
+ id: 2,
27
+ system: SYSTEM_TYPES[:Legacy],
28
+ name: 'Current Conditions Record - Revision B',
29
+ type: :current_conditions
30
+ ),
31
+ RecordType.new(
32
+ id: 3,
33
+ system: SYSTEM_TYPES[:Legacy],
34
+ name: 'Archive Record - Revision A',
35
+ type: :archive
36
+ ),
37
+ RecordType.new(
38
+ id: 4,
39
+ system: SYSTEM_TYPES[:Legacy],
40
+ name: 'Archive Record - Revision B',
41
+ type: :archive
42
+ ),
43
+ RecordType.new(
44
+ id: 5,
45
+ system: SYSTEM_TYPES[:Legacy],
46
+ name: 'High/Low Record (deprecated)',
47
+ type: :high_low
48
+ ),
49
+ RecordType.new(
50
+ id: 6,
51
+ system: SYSTEM_TYPES[:EnviroMonitor],
52
+ name: 'ISS Current Conditions Record',
53
+ type: :current_conditions
54
+ ),
55
+ RecordType.new(
56
+ id: 7,
57
+ system: SYSTEM_TYPES[:EnviroMonitor],
58
+ name: 'ISS Archive Record',
59
+ type: :archive
60
+ ),
61
+ RecordType.new(
62
+ id: 8,
63
+ system: SYSTEM_TYPES[:EnviroMonitor],
64
+ name: 'ISS High/Low Record (deprecated)',
65
+ type: :archive
66
+ ),
67
+ RecordType.new(
68
+ id: 9,
69
+ system: SYSTEM_TYPES[:EnviroMonitor],
70
+ name: 'non-ISS Record',
71
+ type: :unknown
72
+ ),
73
+ RecordType.new(
74
+ id: 10,
75
+ system: SYSTEM_TYPES[:WeatherLinkLive],
76
+ name: 'ISS Current Conditions Record',
77
+ type: :current_conditions
78
+ ),
79
+ RecordType.new(
80
+ id: 11,
81
+ system: SYSTEM_TYPES[:WeatherLinkLive],
82
+ name: 'ISS Archive Record',
83
+ type: :archive
84
+ ),
85
+ RecordType.new(
86
+ id: 12,
87
+ system: SYSTEM_TYPES[:WeatherLinkLive],
88
+ name: 'non-ISS Current Conditions Record',
89
+ type: :current_conditions
90
+ ),
91
+ RecordType.new(
92
+ id: 13,
93
+ system: SYSTEM_TYPES[:WeatherLinkLive],
94
+ name: 'non-ISS Archive Record',
95
+ type: :archive
96
+ ),
97
+ RecordType.new(
98
+ id: 14,
99
+ system: SYSTEM_TYPES[:EnviroMonitor],
100
+ name: 'Health Record',
101
+ type: :health
102
+ ),
103
+ RecordType.new(
104
+ id: 15,
105
+ system: SYSTEM_TYPES[:WeatherLinkLive],
106
+ name: 'Health Record',
107
+ type: :health
108
+ ),
109
+ RecordType.new(
110
+ id: 16,
111
+ system: SYSTEM_TYPES[:AirLink],
112
+ name: 'Current Conditions Record',
113
+ type: :current_conditions
114
+ ),
115
+ RecordType.new(
116
+ id: 17,
117
+ system: SYSTEM_TYPES[:AirLink],
118
+ name: 'Archive Record',
119
+ type: :archive
120
+ ),
121
+ RecordType.new(
122
+ id: 18,
123
+ system: SYSTEM_TYPES[:AirLink],
124
+ name: 'Health Record',
125
+ type: :health
126
+ ),
127
+ ].freeze
128
+
129
+ RECORD_TYPES_BY_ID = RECORD_TYPES.each_with_object({}) { |r, h| h[r.id] = r }
130
+
131
+ def self.record_type(id)
132
+ RECORD_TYPES_BY_ID[id]
133
+ end
134
+
135
+ # TODO: Eliminate duplicate data e.g. rain_rate_last_{in,mm,clicks}
136
+ # TODO: Wind speeds are actually in mph not m/s?
137
+ RECORD_FIELD_UNITS = {
138
+ temp: :temperature,
139
+ temp_in: :temperature,
140
+ dew_point: :temperature,
141
+ dew_point_in: :temperature,
142
+ wet_bulb: :temperature,
143
+ heat_index: :temperature,
144
+ heat_index_in: :temperature,
145
+ wind_chill: :temperature,
146
+ thw_index: :temperature,
147
+ thsw_index: :temperature,
148
+ hum: :humidity,
149
+ hum_in: :humidity,
150
+ bar_sea_level: :pressure,
151
+ bar_absolute: :pressure,
152
+ bar_trend: :pressure,
153
+ wind_speed_last: :wind_speed,
154
+ wind_speed_avg_last_1_min: :wind_speed,
155
+ wind_speed_avg_last_2_min: :wind_speed,
156
+ wind_speed_avg_last_10_min: :wind_speed,
157
+ wind_speed_hi_last_2_min: :wind_speed,
158
+ wind_speed_hi_last_10_min: :wind_speed,
159
+ wind_dir_last: :wind_direction,
160
+ wind_dir_scalar_avg_last_1_min: :wind_direction,
161
+ wind_dir_scalar_avg_last_2_min: :wind_direction,
162
+ wind_dir_scalar_avg_last_10_min: :wind_direction,
163
+ wind_dir_at_hi_speed_last_2_min: :wind_direction,
164
+ wind_dir_at_hi_speed_last_10_min: :wind_direction,
165
+ rain_rate_last: :rain_rate,
166
+ rain_rate_hi: :rain_rate,
167
+ rain_rate_hi_last_15_min: :rain_rate,
168
+ rainfall_last_15_min: :rain_quantity,
169
+ rainfall_last_60_min: :rain_quantity,
170
+ rainfall_last_24_hr: :rain_quantity,
171
+ rainfall_daily: :rain_quantity,
172
+ rainfall_monthly: :rain_quantity,
173
+ rainfall_year: :rain_quantity,
174
+ rain_storm: :rain_quantity,
175
+ rain_storm_last: :rain_quantity,
176
+ solar_rad: :solar_radiation,
177
+ }.freeze
178
+
179
+ attr_reader :api_key, :api_secret, :units
180
+
181
+ def initialize(api_key:, api_secret:, units: IMPERIAL_WEATHER_UNITS)
182
+ @api_key = api_key
183
+ @api_secret = api_secret
184
+ @units = units
185
+ end
186
+
187
+ def type_for(field)
188
+ return nil unless [String, Symbol].include?(field.class)
189
+
190
+ RECORD_FIELD_UNITS.fetch(field.to_sym, nil)
191
+ end
192
+
193
+ def unit_for(field)
194
+ units.fetch(type_for(field))
195
+ end
196
+
197
+ def sensor_catalog
198
+ request(path: 'sensor-catalog')
199
+ end
200
+
201
+ def stations(ids = nil)
202
+ request(path: 'stations', path_params: { 'station-ids' => optional_array_param(ids) })
203
+ end
204
+
205
+ alias station stations
206
+
207
+ def nodes(ids = nil)
208
+ request(path: 'nodes', path_params: { 'node-ids' => optional_array_param(ids) })
209
+ end
210
+
211
+ alias node nodes
212
+
213
+ def sensors(ids = nil)
214
+ request(path: 'sensors', path_params: { 'sensor-ids' => optional_array_param(ids) })
215
+ end
216
+
217
+ alias sensor sensors
218
+
219
+ def sensor_activity(ids = nil)
220
+ request(path: 'sensor-activity', path_params: { 'sensor-ids' => optional_array_param(ids) })
221
+ end
222
+
223
+ def current(id)
224
+ request(path: 'current', path_params: { 'station-id' => id })
225
+ end
226
+
227
+ def historic(id, start_timestamp, end_timestamp)
228
+ request(
229
+ path: 'historic',
230
+ path_params: { 'station-id' => id },
231
+ query_params: { 'start-timestamp' => start_timestamp, 'end-timestamp' => end_timestamp }
232
+ )
233
+ end
234
+
235
+ def last_seconds(id, seconds)
236
+ historic(id, Time.now.to_i - seconds, Time.now.to_i)
237
+ end
238
+
239
+ def last_hour(id)
240
+ last_seconds(id, 3600)
241
+ end
242
+
243
+ def last_day(id)
244
+ last_seconds(id, 86_400)
245
+ end
246
+
247
+ def request(path:, path_params: {}, query_params: {})
248
+ uri = request_uri(path: path, path_params: path_params, query_params: query_params)
249
+ response = Net::HTTP.get_response(uri)
250
+ JSON.parse(response.body)
251
+ end
252
+
253
+ def request_uri(path:, path_params: {}, query_params: {})
254
+ used_path_params = path_params.compact
255
+
256
+ request_params = query_params.merge({ 't' => Time.now.to_i, 'api-key' => api_key })
257
+ request_params.merge!(
258
+ {
259
+ 'api-signature' => api_signature(path_params: used_path_params, query_params: request_params),
260
+ }
261
+ )
262
+
263
+ uri = ([BASE_URI, path] + Array(used_path_params.values)).compact.join('/')
264
+
265
+ URI("#{uri}?#{URI.encode_www_form(request_params)}")
266
+ end
267
+
268
+ # private
269
+
270
+ def optional_array_param(param)
271
+ param.is_a?(Array) ? param.join(',') : param
272
+ end
273
+
274
+ def stuffed_params(params)
275
+ params.sort_by { |k, _| k }.map { |k, v| k.to_s + v.to_s }.join
276
+ end
277
+
278
+ def api_signature(path_params: {}, query_params: {})
279
+ OpenSSL::HMAC.hexdigest('SHA256', api_secret, stuffed_params(path_params.merge(query_params)))
280
+ end
281
+ end
282
+ end