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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.rubocop.yml +27 -0
- data/.travis.yml +6 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/weatherlink.rb +86 -0
- data/lib/weatherlink/api_v2.rb +282 -0
- data/lib/weatherlink/client.rb +64 -0
- data/lib/weatherlink/data_record.rb +12 -0
- data/lib/weatherlink/hash_wrapper.rb +26 -0
- data/lib/weatherlink/local_api_v1.rb +143 -0
- data/lib/weatherlink/local_client.rb +60 -0
- data/lib/weatherlink/node.rb +24 -0
- data/lib/weatherlink/sensor.rb +24 -0
- data/lib/weatherlink/sensor_data.rb +60 -0
- data/lib/weatherlink/sensor_data_collection.rb +36 -0
- data/lib/weatherlink/sensor_record.rb +39 -0
- data/lib/weatherlink/station.rb +71 -0
- data/lib/weatherlink/version.rb +5 -0
- data/weatherlink.gemspec +37 -0
- metadata +115 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WeatherLink
|
4
|
+
class Client
|
5
|
+
attr_reader :api
|
6
|
+
|
7
|
+
def initialize(api_key:, api_secret:, station_units: IMPERIAL_WEATHER_UNITS, desired_units: METRIC_WEATHER_UNITS)
|
8
|
+
@api = APIv2.new(api_key: api_key, api_secret: api_secret, units: station_units)
|
9
|
+
@desired_units = desired_units
|
10
|
+
end
|
11
|
+
|
12
|
+
def attach_units(data)
|
13
|
+
data.map do |field, value|
|
14
|
+
unit = api.unit_for(field)
|
15
|
+
[field, unit ? Unit.new("#{value} #{unit}") : value]
|
16
|
+
end.to_h
|
17
|
+
end
|
18
|
+
|
19
|
+
def desired_unit_for(field)
|
20
|
+
desired_units.fetch(api.type_for(field))
|
21
|
+
end
|
22
|
+
|
23
|
+
def convert(field, value)
|
24
|
+
desired_unit = desired_unit_for(field)
|
25
|
+
return value unless desired_unit
|
26
|
+
|
27
|
+
value.convert_to(desired_unit)
|
28
|
+
end
|
29
|
+
|
30
|
+
def stations
|
31
|
+
@stations ||= api.station['stations'].map do |data|
|
32
|
+
Station.new(self, data) if data
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def station
|
37
|
+
stations.first
|
38
|
+
end
|
39
|
+
|
40
|
+
def stations_by_device_id_hex(device_id_hex)
|
41
|
+
stations.select { |s| s.gateway_id_hex == device_id_hex }.first
|
42
|
+
end
|
43
|
+
|
44
|
+
def nodes
|
45
|
+
@nodes ||= api.nodes['nodes'].map do |data|
|
46
|
+
Node.new(self, data)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def node_by_device_id_hex(device_id_hex)
|
51
|
+
nodes.select { |n| n.device_id_hex == device_id_hex }.first
|
52
|
+
end
|
53
|
+
|
54
|
+
def sensors
|
55
|
+
@sensors ||= api.sensors['sensors'].map do |data|
|
56
|
+
Sensor.new(self, data)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def sensor_by_lsid(lsid)
|
61
|
+
sensors.select { |s| s.lsid == lsid }.first
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WeatherLink
|
4
|
+
class HashWrapper < SimpleDelegator
|
5
|
+
attr_reader :data
|
6
|
+
|
7
|
+
def initialize(data)
|
8
|
+
@data = data
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def method_missing(symbol, *args)
|
15
|
+
return data.fetch(symbol.to_s) if data.include?(symbol.to_s)
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def respond_to_missing?(symbol, include_private = false)
|
21
|
+
return true if data.include?(symbol.to_s)
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module WeatherLink
|
7
|
+
class LocalAPIv1
|
8
|
+
class RequestError < StandardError; end
|
9
|
+
|
10
|
+
LOCAL_API = SystemType.new(name: 'Local API')
|
11
|
+
|
12
|
+
RECORD_TYPES = [
|
13
|
+
RecordType.new(
|
14
|
+
id: 1,
|
15
|
+
system: LOCAL_API,
|
16
|
+
name: 'ISS Record',
|
17
|
+
type: :current_conditions
|
18
|
+
),
|
19
|
+
RecordType.new(
|
20
|
+
id: 2,
|
21
|
+
system: LOCAL_API,
|
22
|
+
name: 'Leaf/Soil Moisture Record',
|
23
|
+
type: :current_conditions
|
24
|
+
),
|
25
|
+
RecordType.new(
|
26
|
+
id: 3,
|
27
|
+
system: LOCAL_API,
|
28
|
+
name: 'LSS Barometric Pressure Record',
|
29
|
+
type: :current_conditions
|
30
|
+
),
|
31
|
+
RecordType.new(
|
32
|
+
id: 4,
|
33
|
+
system: LOCAL_API,
|
34
|
+
name: 'LSS Temperature/Humidity Record',
|
35
|
+
type: :current_conditions
|
36
|
+
),
|
37
|
+
RecordType.new(
|
38
|
+
id: 6,
|
39
|
+
system: LOCAL_API,
|
40
|
+
name: 'AirLink Record',
|
41
|
+
type: :current_conditions
|
42
|
+
),
|
43
|
+
].freeze
|
44
|
+
|
45
|
+
RECORD_TYPES_BY_ID = RECORD_TYPES.each_with_object({}) { |r, h| h[r.id] = r }
|
46
|
+
|
47
|
+
def self.record_type(id)
|
48
|
+
RECORD_TYPES_BY_ID[id]
|
49
|
+
end
|
50
|
+
|
51
|
+
RECORD_FIELD_UNITS = {
|
52
|
+
temp: :temperature,
|
53
|
+
temp_in: :temperature,
|
54
|
+
dew_point: :temperature,
|
55
|
+
dew_point_in: :temperature,
|
56
|
+
wet_bulb: :temperature,
|
57
|
+
heat_index: :temperature,
|
58
|
+
heat_index_in: :temperature,
|
59
|
+
wind_chill: :temperature,
|
60
|
+
thw_index: :temperature,
|
61
|
+
thsw_index: :temperature,
|
62
|
+
hum: :humidity,
|
63
|
+
hum_in: :humidity,
|
64
|
+
bar_sea_level: :pressure,
|
65
|
+
bar_absolute: :pressure,
|
66
|
+
bar_trend: :pressure,
|
67
|
+
wind_speed_last: :wind_speed,
|
68
|
+
wind_speed_avg_last_1_min: :wind_speed,
|
69
|
+
wind_speed_avg_last_2_min: :wind_speed,
|
70
|
+
wind_speed_avg_last_10_min: :wind_speed,
|
71
|
+
wind_speed_hi_last_2_min: :wind_speed,
|
72
|
+
wind_speed_hi_last_10_min: :wind_speed,
|
73
|
+
wind_dir_last: :wind_direction,
|
74
|
+
wind_dir_scalar_avg_last_1_min: :wind_direction,
|
75
|
+
wind_dir_scalar_avg_last_2_min: :wind_direction,
|
76
|
+
wind_dir_scalar_avg_last_10_min: :wind_direction,
|
77
|
+
wind_dir_at_hi_speed_last_2_min: :wind_direction,
|
78
|
+
wind_dir_at_hi_speed_last_10_min: :wind_direction,
|
79
|
+
rain_rate_last: :rain_rate,
|
80
|
+
rain_rate_hi: :rain_rate,
|
81
|
+
rain_rate_hi_last_15_min: :rain_rate,
|
82
|
+
rainfall_last_15_min: :rain_quantity,
|
83
|
+
rainfall_last_60_min: :rain_quantity,
|
84
|
+
rainfall_last_24_hr: :rain_quantity,
|
85
|
+
rainfall_daily: :rain_quantity,
|
86
|
+
rainfall_monthly: :rain_quantity,
|
87
|
+
rainfall_year: :rain_quantity,
|
88
|
+
rain_storm: :rain_quantity,
|
89
|
+
rain_storm_last: :rain_quantity,
|
90
|
+
solar_rad: :solar_radiation,
|
91
|
+
}.freeze
|
92
|
+
|
93
|
+
attr_reader :host, :units
|
94
|
+
|
95
|
+
def initialize(host:, units: IMPERIAL_WEATHER_UNITS)
|
96
|
+
@host = host
|
97
|
+
@units = units
|
98
|
+
end
|
99
|
+
|
100
|
+
def type_for(field)
|
101
|
+
return nil unless [String, Symbol].include?(field.class)
|
102
|
+
|
103
|
+
RECORD_FIELD_UNITS.fetch(field.to_sym, nil)
|
104
|
+
end
|
105
|
+
|
106
|
+
def unit_for(field)
|
107
|
+
units.fetch(type_for(field))
|
108
|
+
end
|
109
|
+
|
110
|
+
def current_conditions
|
111
|
+
request(path: 'current_conditions')
|
112
|
+
end
|
113
|
+
|
114
|
+
def request(path:, path_params: {}, query_params: {})
|
115
|
+
uri = request_uri(path: path, path_params: path_params, query_params: query_params)
|
116
|
+
response = Net::HTTP.get_response(uri)
|
117
|
+
json_response = JSON.parse(response.body)
|
118
|
+
raise RequestError, json_response['error'] if json_response['error']
|
119
|
+
|
120
|
+
json_response['data']
|
121
|
+
end
|
122
|
+
|
123
|
+
def base_uri
|
124
|
+
"http://#{host}/v1"
|
125
|
+
end
|
126
|
+
|
127
|
+
def request_uri(path:, path_params: {}, query_params: {})
|
128
|
+
uri = ([base_uri, path] + Array(path_params.values)).compact.join('/')
|
129
|
+
|
130
|
+
if query_params.none?
|
131
|
+
URI(uri)
|
132
|
+
else
|
133
|
+
URI("#{uri}?#{URI.encode_www_form(query_params)}")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# private
|
138
|
+
|
139
|
+
def optional_array_param(param)
|
140
|
+
param.is_a?(Array) ? param.join(',') : param
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ruby-units'
|
4
|
+
|
5
|
+
module WeatherLink
|
6
|
+
class LocalClient
|
7
|
+
attr_reader :api, :desired_units
|
8
|
+
|
9
|
+
def initialize(host:, station_units: IMPERIAL_WEATHER_UNITS, desired_units: METRIC_WEATHER_UNITS)
|
10
|
+
@api = LocalAPIv1.new(host: host, units: station_units)
|
11
|
+
@desired_units = desired_units
|
12
|
+
end
|
13
|
+
|
14
|
+
META_KEYS = %w[lsid data_structure_type txid].freeze
|
15
|
+
|
16
|
+
def transform_like_api_v2(hash)
|
17
|
+
hash.select { |k, _| META_KEYS.include?(k) }.merge('data' => [{
|
18
|
+
'ts' => Time.now.to_i,
|
19
|
+
}.merge(hash.reject { |k, _| META_KEYS.include?(k) })])
|
20
|
+
end
|
21
|
+
|
22
|
+
def attach_units(data)
|
23
|
+
data.map do |field, value|
|
24
|
+
unit = api.unit_for(field)
|
25
|
+
[field, unit ? Unit.new("#{value} #{unit}") : value]
|
26
|
+
end.to_h
|
27
|
+
end
|
28
|
+
|
29
|
+
def desired_unit_for(field)
|
30
|
+
desired_units.fetch(api.type_for(field))
|
31
|
+
end
|
32
|
+
|
33
|
+
def convert(field, value)
|
34
|
+
desired_unit = desired_unit_for(field)
|
35
|
+
return value unless desired_unit
|
36
|
+
|
37
|
+
value.convert_to(desired_unit)
|
38
|
+
end
|
39
|
+
|
40
|
+
def current_conditions
|
41
|
+
sensors = api.current_conditions['conditions'].map do |conditions|
|
42
|
+
SensorData.new(self, transform_like_api_v2(conditions))
|
43
|
+
end
|
44
|
+
|
45
|
+
SensorDataCollection.new(self, sensors)
|
46
|
+
end
|
47
|
+
|
48
|
+
def stations
|
49
|
+
@stations ||= api.station['stations'].map do |data|
|
50
|
+
Station.new(self, data) if data
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def sensors
|
55
|
+
@sensors ||= api.sensors['sensors'].map do |data|
|
56
|
+
Sensor.new(self, data)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WeatherLink
|
4
|
+
class Node < HashWrapper
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client, data)
|
8
|
+
@client = client
|
9
|
+
super(data)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"#<#{self.class.name} device_id_hex=#{device_id_hex} (#{description})>"
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def description
|
21
|
+
"#{station_name} - #{node_name}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WeatherLink
|
4
|
+
class Sensor < HashWrapper
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client, data)
|
8
|
+
@client = client
|
9
|
+
super(data)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"#<#{self.class.name} lsid=#{lsid} (#{description})>"
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def description
|
21
|
+
"#{manufacturer} - #{product_name}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WeatherLink
|
4
|
+
class SensorData < SimpleDelegator
|
5
|
+
attr_reader :client, :records, :sensor_data
|
6
|
+
|
7
|
+
def initialize(client, sensor_data)
|
8
|
+
@client = client
|
9
|
+
@sensor_data = HashWrapper.new(sensor_data)
|
10
|
+
@records = @sensor_data['data'].map do |data|
|
11
|
+
SensorRecord.new(client, client.attach_units(data))
|
12
|
+
end
|
13
|
+
super(@records.first)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#<#{self.class.name} lsid=#{lsid} (#{record_type.description}, #{records.size} records)>"
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def lsid
|
25
|
+
sensor_data.lsid
|
26
|
+
end
|
27
|
+
|
28
|
+
def sensor
|
29
|
+
@sensor ||= @client.sensors.select { |sensor| sensor.lsid == lsid }.first
|
30
|
+
end
|
31
|
+
|
32
|
+
def sensor_type
|
33
|
+
sensor_data.sensor_type
|
34
|
+
end
|
35
|
+
|
36
|
+
def record_type
|
37
|
+
@record_type ||= client.api.class.record_type(sensor_data.data_structure_type)
|
38
|
+
end
|
39
|
+
|
40
|
+
def health?
|
41
|
+
record_type.health?
|
42
|
+
end
|
43
|
+
|
44
|
+
def current_conditions?
|
45
|
+
record_type.current_conditions?
|
46
|
+
end
|
47
|
+
|
48
|
+
def archive?
|
49
|
+
record_type.archive?
|
50
|
+
end
|
51
|
+
|
52
|
+
def weather?
|
53
|
+
record_type.current_conditions? || record_type.archive?
|
54
|
+
end
|
55
|
+
|
56
|
+
def description
|
57
|
+
record_type.description
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WeatherLink
|
4
|
+
class SensorDataCollection < SimpleDelegator
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def initialize(client, sensors)
|
8
|
+
@client = client
|
9
|
+
super(sensors)
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_s
|
13
|
+
"#<#{self.class.name} (#{size} sensors)>"
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_conditions
|
21
|
+
SensorDataCollection.new(client, select(&:current_conditions?))
|
22
|
+
end
|
23
|
+
|
24
|
+
def archive
|
25
|
+
SensorDataCollection.new(client, select(&:archive?))
|
26
|
+
end
|
27
|
+
|
28
|
+
def weather
|
29
|
+
SensorDataCollection.new(client, select(&:weather?))
|
30
|
+
end
|
31
|
+
|
32
|
+
def health
|
33
|
+
SensorDataCollection.new(client, select(&:health?))
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|