Module: Timely::Weather

Defined in:
lib/timely/weather.rb

Constant Summary collapse

SYMBOLS =
{
  0 => "\u2600",   # Clear sky
  1 => "\u{1F324}", # Mostly clear
  2 => "\u26C5",   # Partly cloudy
  3 => "\u2601",   # Cloudy
  5 => "\u{1F326}", # Rain showers
  6 => "\u{1F327}", # Rain
  8 => "\u{1F328}", # Snow
  9 => "\u{1F329}", # Thunder
}.freeze

Class Method Summary collapse

Class Method Details

.fetch(lat, lon, db = nil) ⇒ Object

Fetch weather forecast from met.no Returns hash of date_str => { temp_high:, temp_low:, symbol:, wind:, description: }



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/timely/weather.rb', line 20

def self.fetch(lat, lon, db = nil)
  # Check cache first (6 hour TTL)
  if db
    cached = db.execute(
      "SELECT data, fetched_at FROM weather_cache WHERE date = 'forecast' LIMIT 1"
    ).first
    if cached && (Time.now.to_i - cached['fetched_at'].to_i) < 21600
      return JSON.parse(cached['data']) rescue {}
    end
  end

  uri = URI("https://api.met.no/weatherapi/locationforecast/2.0/complete?lat=#{lat}&lon=#{lon}")
  req = Net::HTTP::Get.new(uri)
  req['User-Agent'] = 'timely-calendar/0.1 [email protected]'
  req['Accept-Encoding'] = 'identity'

  res = Net::HTTP.start(uri.hostname, uri.port,
                        use_ssl: true,
                        read_timeout: 10,
                        open_timeout: 5) do |http|
    http.request(req)
  end

  return {} unless res.is_a?(Net::HTTPSuccess)

  series = JSON.parse(res.body).dig('properties', 'timeseries') || []

  # Group by date, find high/low temps and midday conditions
  by_date = {}
  series.each do |ts|
    det = ts.dig('data', 'instant', 'details')
    next unless det
    time = ts['time']
    date = time[0..9]
    hour = time[11..12].to_i
    temp = det['air_temperature'].to_f

    by_date[date] ||= { temps: [], wind: 0, cloud: 0, midday_temp: nil }
    by_date[date][:temps] << temp
    # Capture midday (12:00) conditions for the symbol
    if hour == 12
      by_date[date][:midday_temp] = temp
      by_date[date][:wind] = det['wind_speed'].to_f.round(1)
      by_date[date][:cloud] = det['cloud_area_fraction'].to_i
    end
  end

  forecast = {}
  by_date.each do |date, data|
    cloud = data[:cloud]
    symbol = if cloud < 15
      SYMBOLS[0]
    elsif cloud < 40
      SYMBOLS[1]
    elsif cloud < 70
      SYMBOLS[2]
    else
      SYMBOLS[3]
    end

    temps = data[:temps]
    forecast[date] = {
      'temp_high' => temps.max.round(1),
      'temp_low' => temps.min.round(1),
      'temp_mid' => (data[:midday_temp] || temps[temps.size / 2]).round(1),
      'symbol' => symbol,
      'wind' => data[:wind],
      'cloud' => cloud
    }
  end

  # Cache result
  if db
    db.execute(
      "INSERT OR REPLACE INTO weather_cache (date, hour, data, fetched_at) VALUES (?, ?, ?, ?)",
      ['forecast', '00', JSON.generate(forecast), Time.now.to_i]
    )
  end

  forecast
rescue SocketError, Timeout::Error, Net::OpenTimeout, Errno::ECONNREFUSED => e
  {}
end

.short_for_date(forecast, date) ⇒ Object

Get a short weather string for a date: "☀ 12°"



105
106
107
108
109
110
# File 'lib/timely/weather.rb', line 105

def self.short_for_date(forecast, date)
  date_str = date.strftime('%Y-%m-%d')
  w = forecast[date_str]
  return nil unless w
  "#{w['symbol']} #{w['temp_mid']}°"
end