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,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