weatherlink 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WeatherLink
4
+ class DataRecord < HashWrapper
5
+ attr_reader :client
6
+
7
+ def initialize(client, data)
8
+ @client = client
9
+ super(data)
10
+ end
11
+ end
12
+ 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