yr_weather 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/yr_weather.rb +362 -0
  3. metadata +46 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bbf68e1623f53a5a3ecb6d38a5c79d20167aa70b5d06f18dfc8a672658bd0070
4
+ data.tar.gz: cbb5cc94eaf848499f757ce9c7ba45d05f6e1f6dddd931eb4d8f96ae460104b9
5
+ SHA512:
6
+ metadata.gz: 761d8bac6187feff85ff34980a0149ea8dfe986fe825986fcba2ab8569d78ecd072370a63b7e68f7a8e124dcabbad98fea365e84ee8ba5075dc2316a583b32e8
7
+ data.tar.gz: e76bf5b6974cb8eed9fc9c26ce488e38c2b8a5a2e791568e08a7a17dba022929633823261945b988cb316d84349040cf32b7a715b37c7dc01c38605b16c5f83c
@@ -0,0 +1,362 @@
1
+ require 'json'
2
+ require 'time'
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'tmpdir'
6
+
7
+ class YrWeather
8
+
9
+ class << self
10
+ attr_accessor :configuration
11
+ end
12
+
13
+ def self.config
14
+ self.configuration ||= YrWeather::Configuration.new
15
+ yield(configuration)
16
+ end
17
+
18
+ class YrWeather::Configuration
19
+ attr_accessor :sitename
20
+ attr_accessor :redis
21
+ attr_accessor :utc_offset
22
+ def initialize
23
+ @sitename = nil
24
+ @redis = nil
25
+ @utc_offset = nil
26
+ end
27
+ end
28
+
29
+ class YrWeather::RedisCache
30
+
31
+ def initialize(params)
32
+ @latitude = params[:latitude]
33
+ @longitude = params[:longitude]
34
+ @redis = params[:redis]
35
+ end
36
+
37
+ def to_cache(data)
38
+ seconds_to_cache = (data[:expires] - Time.now).ceil
39
+ seconds_to_cache = 60 if seconds_to_cache < 60
40
+ @redis.set(redis_key, data.to_json, ex: seconds_to_cache)
41
+ end
42
+
43
+ def from_cache
44
+ @redis.get(redis_key)
45
+ end
46
+
47
+ private
48
+
49
+ def redis_key
50
+ "yr_weather.#{@latitude}.#{@longitude}"
51
+ end
52
+
53
+ end
54
+
55
+ class YrWeather::FileCache
56
+
57
+ def initialize(params)
58
+ @latitude = params[:latitude]
59
+ @longitude = params[:longitude]
60
+ end
61
+
62
+ def to_cache(data)
63
+ file_name = cache_file_name
64
+ File.write(file_name, data.to_json)
65
+ end
66
+
67
+ def from_cache
68
+ file_name = cache_file_name
69
+ if File.file?(cache_file_name)
70
+ File.read(file_name)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def cache_file_name
77
+ file_name = "yr_weather.#{@latitude}.#{@longitude}.tmp"
78
+ File.join(Dir.tmpdir,file_name)
79
+ end
80
+
81
+ end
82
+
83
+
84
+ YR_NO = 'https://api.met.no/weatherapi/locationforecast/2.0/complete'
85
+ COMPASS_BEARINGS = %w(N NE E SE S SW W NW)
86
+ ARC = 360.0/COMPASS_BEARINGS.length.to_f
87
+
88
+ @latitude = nil
89
+ @longitude = nil
90
+ @utc_offset = nil
91
+
92
+ @data = nil
93
+ @now = Time.now
94
+ @start_of_day = nil
95
+
96
+ # @latitude = -33.9531096408383
97
+ # @longitude = 18.4806353422955
98
+
99
+ # YrWeather.get(latitude: -33.9531096408383, longitude: 18.4806353422955)
100
+ # def self.get(sitename:, latitude:, longitude:, utc_offset: '+00:00', limit_to: nil)
101
+ # YrWeather.new(latitude: latitude, longitude: longitude, utc_offset: utc_offset).process(limit_to)
102
+ # end
103
+
104
+ def initialize(latitude:, longitude:, utc_offset: nil)
105
+ @latitude = latitude.round(4) # yr developer page requests four decimals.
106
+ @longitude = longitude.round(4)
107
+ @utc_offset = utc_offset || YrWeather.configuration.utc_offset || '+00:00'
108
+ @now = Time.now.localtime(@utc_offset)
109
+ @start_of_day = Time.local(@now.year, @now.month, @now.day)
110
+ @start_of_day = @start_of_day + 24*60*60 if @now.hour >= 20
111
+ raise 'yr.no reqiure a sitename and email. See readme for details' unless YrWeather.configuration.sitename=~/@/
112
+ params = { latitude: @latitude, longitude: @longitude, redis: YrWeather.configuration.redis }
113
+ @cacher = (YrWeather.configuration.redis ? YrWeather::RedisCache.new(params) : YrWeather::FileCache.new(params))
114
+ @data = load_forecast
115
+ end
116
+
117
+ def initialised?
118
+ !@data.nil?
119
+ end
120
+
121
+ def raw
122
+ @data
123
+ end
124
+
125
+ def metadata
126
+ {
127
+ forecast_updated_at: @data.dig(:properties, :meta, :updated_at),
128
+ downloaded_at: @data[:downloaded_at],
129
+ expires_at: @data[:expires],
130
+ start_of_day: @start_of_day,
131
+ latitude: @data.dig(:geometry, :coordinates)[1],
132
+ longitude: @data.dig(:geometry, :coordinates)[0],
133
+ msl: @data.dig(:geometry, :coordinates)[2],
134
+ units: @data.dig(:properties, :meta, :units),
135
+ }
136
+ end
137
+
138
+ def current
139
+ time = @data.dig(:properties, :timeseries).map { |e| e[:time] }.reject { |e| e>@now }.sort.last
140
+ node = @data.dig(:properties, :timeseries).select { |e| e[:time]==time }.first
141
+ node.dig(:data, :instant, :details).merge({
142
+ at: time,
143
+ symbol_code: node.dig(:data, :next_1_hours, :summary, :symbol_code),
144
+ precipitation_amount: node.dig(:data, :next_1_hours, :details, :precipitation_amount),
145
+ wind_from_direction: degrees_to_bearing(node.dig(:data, :instant, :details, :wind_from_direction)),
146
+ wind_description: wind_description(node.dig(:data, :instant, :details, :wind_speed)),
147
+ wind_speed_knots: to_knots(node.dig(:data, :instant, :details, :wind_speed)),
148
+ })
149
+ end
150
+
151
+ def next_12_hours
152
+ range = @now..(@now + 12*60*60)
153
+ forecast(range).merge(symbol: symbol_code_hourly(range))
154
+ end
155
+
156
+ def three_days
157
+ range = @now..(@now + 3*24*60*60)
158
+ forecast(range).tap { |hs| hs.delete(:wind_description) }
159
+ end
160
+
161
+ def week
162
+ range = @now..(@now + 7*24*60*60)
163
+ forecast(range).tap { |hs| hs.delete(:wind_description) }
164
+ end
165
+
166
+ def six_hourly
167
+ t = @start_of_day
168
+ loop do
169
+ if (t + 6*60*60) > Time.now
170
+ break
171
+ else
172
+ t = t + 6*60*60
173
+ end
174
+ end
175
+ nodes = @data.dig(:properties, :timeseries).select { |e| e.dig(:data, :next_6_hours) }.map { |e| [e[:time], e] }.to_h
176
+ nodes = 20.times.map do |i|
177
+ nodes[t + i*6*60*60]
178
+ end.compact.map do |node|
179
+ {
180
+ at: node.dig(:time),
181
+ temperature_maximum: node.dig(:data, :next_6_hours, :details, :air_temperature_max),
182
+ temperature_minimum: node.dig(:data, :next_6_hours, :details, :air_temperature_min),
183
+ wind_speed_max: node.dig(:data, :instant, :details, :wind_speed),
184
+ wind_speed_max_knots: to_knots(node.dig(:data, :instant, :details, :wind_speed)),
185
+ wind_direction: degrees_to_bearing(node.dig(:data, :instant, :details, :wind_from_direction)),
186
+ wind_description: node.dig(:data, :instant, :details, :wind_speed),
187
+ precipitation: node.dig(:data, :next_6_hours, :details, :precipitation_amount),
188
+ symbol_code: node.dig(:data, :next_6_hours, :summary, :symbol_code),
189
+ }
190
+ end
191
+ end
192
+
193
+ def daily
194
+ 8.times.map do |day|
195
+ start = @start_of_day + day*24*60*60
196
+ range = start..(start + 24*60*60)
197
+ forecast(range).merge(at: start)
198
+ end
199
+ end
200
+
201
+ def arrays
202
+ nodes = @data.dig(:properties, :timeseries)
203
+ points = nodes.map do |node|
204
+ {
205
+ at: node[:time],
206
+ temperature: node.dig(:data, :instant, :details, :air_temperature),
207
+ wind_speed: node.dig(:data, :instant, :details, :wind_speed),
208
+ precipitation: node.dig(:data, :next_1_hours, :details, :precipitation_amount) || node.dig(:data, :next_6_hours, :details, :precipitation_amount),
209
+ hours: ( node.dig(:data, :next_1_hours, :details, :precipitation_amount) ? 1 : 6),
210
+ }
211
+ end
212
+ results = {
213
+ at: [],
214
+ temperature: [],
215
+ wind_speed: [],
216
+ wind_speed_knots: [],
217
+ precipitation: [],
218
+ hours: [],
219
+ }
220
+ points.each do |point|
221
+ point[:hours].times do |i|
222
+ results[:at] << point[:at] + i*60*60
223
+ results[:temperature] << point[:temperature]
224
+ results[:wind_speed] << point[:wind_speed]
225
+ results[:wind_speed_knots] << to_knots(point[:wind_speed])
226
+ results[:precipitation] << ((point[:precipitation].to_f) / (point[:hours].to_f)).round(1)
227
+ end
228
+ end
229
+ results
230
+ end
231
+
232
+
233
+ private
234
+
235
+ def forecast(range)
236
+ nodes = nodes_for_range(range)
237
+ detail = nodes.map { |e| e.dig(:data, :instant, :details) }
238
+ wind_directions = detail.map { |e| degrees_to_bearing(e[:wind_from_direction]) }
239
+ {
240
+ temperature_maximum: detail.map { |e| e[:air_temperature] }.max,
241
+ temperature_minimum: detail.map { |e| e[:air_temperature] }.min,
242
+ wind_speed_max: detail.map { |e| e[:wind_speed] }.max,
243
+ wind_speed_max_knots: to_knots(detail.map { |e| e[:wind_speed] }.max),
244
+ wind_description: wind_description(detail.map { |e| e[:wind_speed] }.max),
245
+ wind_direction: wind_directions.max_by { |e| wind_directions.count(e) },
246
+ precipitation: precipitation(range, nodes)
247
+ }
248
+ end
249
+
250
+ def precipitation(range, nodes)
251
+ next_time = range.first
252
+ end_time = range.last
253
+ nodes.map do |node|
254
+ mm = nil
255
+ if node[:time] >= next_time
256
+ [1,6,12].each do |i|
257
+ mm = node.dig(:data, "next_#{i}_hours".to_sym, :details, :precipitation_amount)
258
+ if mm
259
+ next_time = next_time + i*60*60
260
+ break
261
+ end
262
+ end
263
+ end
264
+ mm
265
+ end.sum
266
+ end
267
+
268
+ def symbol_code_hourly(range)
269
+ symbols = nodes_for_range(@now..(@now + 12*60*60)).map { |e| e.dig(:data, :next_1_hours, :summary, :symbol_code) }
270
+ symbols.max_by { |e| symbols.count(e) }
271
+ end
272
+
273
+ def nodes_for_range(range)
274
+ @data.dig(:properties, :timeseries).select { |e| range.include?(e[:time]) }
275
+ end
276
+
277
+ def degrees_to_bearing(degrees)
278
+ COMPASS_BEARINGS[(degrees.to_f/ARC).round % COMPASS_BEARINGS.length]
279
+ end
280
+
281
+ def to_knots(ms)
282
+ ( ms ? (ms*1.943844).round(1) : nil )
283
+ end
284
+
285
+ def wind_description(speed)
286
+ ms = speed.round(1)
287
+ case ms
288
+ when (0..(0.5)) then 'calm'
289
+ when ((0.5)..(1.5)) then 'light air'
290
+ when ((1.6)..(3.3)) then 'light breeze'
291
+ when ((4)..(5.5)) then 'gentle breeze'
292
+ when ((5.5)..(7.9)) then 'moderate breeze'
293
+ when ((8)..(10.7)) then 'fresh breeze'
294
+ when ((10.8)..(13.8)) then 'strong breeze'
295
+ when ((13.9)..(17.1)) then 'high wind,'
296
+ when ((17.2)..(20.7)) then 'gale'
297
+ when ((20.8)..(24.4)) then 'strong gale'
298
+ when ((24.5)..(28.4)) then 'storm'
299
+ when ((28.5)..(32.6)) then 'violent storm'
300
+ else 'hurricane force'
301
+ end
302
+ end
303
+
304
+
305
+ def load_forecast
306
+ data = @cacher.from_cache
307
+ data = parse_json(data) if !data.nil?
308
+ if data.nil?
309
+ data = forecast_from_yr
310
+ @cacher.to_cache(data)
311
+ end
312
+ data
313
+ end
314
+
315
+ def parse_json(json)
316
+ parse_times(JSON.parse(json, symbolize_names: true))
317
+ end
318
+
319
+
320
+ def parse_times(hash)
321
+ if (hash.is_a?(Hash))
322
+ hash.transform_values do |v|
323
+ if v.is_a?(Hash)
324
+ parse_times(v)
325
+ elsif v.is_a?(Array)
326
+ v.map { |e| parse_times(e) }
327
+ elsif v.is_a?(String) && v=~/\d{4}-\d\d-\d\d[\sT]\d\d:\d\d:\d\d/
328
+ Time.parse(v)
329
+ # r = Time.parse(v) rescue nil
330
+ # (r || v)
331
+ else
332
+ v
333
+ end
334
+ end
335
+ else
336
+ hash
337
+ end
338
+ end
339
+
340
+
341
+
342
+ # def parse
343
+ # %w(hourly today tomorrow three_days week daily daily_objects hourly_objects).map(&:to_sym).map { |e| [e, self.send(e)] }
344
+ # end
345
+
346
+ def forecast_from_yr
347
+ url = URI("#{YR_NO}?lat=#{@latitude}&lon=#{@longitude}")
348
+ https = Net::HTTP.new(url.host, url.port)
349
+ https.use_ssl = true
350
+ request = Net::HTTP::Get.new(url)
351
+ request["Content-Type"] = "application/json"
352
+ request["User-Agent"] = YrWeather.configuration.sitename
353
+ response = https.request(request)
354
+ {
355
+ expires: Time.parse(response['expires']),
356
+ last_modified: Time.parse(response['last-modified']),
357
+ downloaded_at: Time.now,
358
+ }.merge(parse_json(response.body))
359
+ end
360
+
361
+
362
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yr_weather
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - renen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-01-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: This gem leverages the yr.no weather API to convert location data into
14
+ hourly forecasts, and summaries that are simpler to understand, and easy to script
15
+ into databases and systems.
16
+ email:
17
+ - renen@121.co.za
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/yr_weather.rb
23
+ homepage: https://github.com/sasa-solutions/yr_parser
24
+ licenses:
25
+ - MIT
26
+ metadata: {}
27
+ post_install_message:
28
+ rdoc_options: []
29
+ require_paths:
30
+ - lib
31
+ required_ruby_version: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ requirements: []
42
+ rubygems_version: 3.0.1
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: Easily interpret and use yr.no weather forecast APIs
46
+ test_files: []