Class: Weather::VisualCrossingClient

Inherits:
Object
  • Object
show all
Defined in:
app/services/weather/visual_crossing_client.rb

Overview

Client for the Visual Crossing Timeline Weather API.

Supports current conditions, historical data, and forecasts for any location
(city name, postal code, address, or lat/lng) and any date range.

The source field on each day tells the LLM whether data comes from:
"obs" = historical weather station observations
"fcst" = 15-day model forecast
"histfcst" = historical forecast model
"stats" = long-term statistical forecast (beyond 15 days)
"comb" = mix of sources (e.g. today = obs + fcst)

Examples:

Current weather / 15-day forecast

client = Weather::VisualCrossingClient.new
result = client.lookup(location: 'Chicago, IL')

Historical date range

result = client.lookup(location: '60601', start_date: '2025-01-01', end_date: '2025-01-31')

Metric units

result = client.lookup(location: 'Montreal, QC', unit_group: 'metric')

See Also:

Defined Under Namespace

Classes: Response

Constant Summary collapse

BASE_URL =
'https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/'
UNIT_GROUPS =
%w[us metric uk base].freeze
DAY_ELEMENTS =

Daily elements. source is critical context: tells the LLM whether each day is
observed historical data, a model forecast, or a statistical projection.

%w[
  datetime temp tempmax tempmin feelslike feelslikemax feelslikemin
  humidity dew precip precipprob preciptype precipcover
  snow snowdepth windspeed windgust winddir
  conditions description source
  sunrise sunset uvindex cloudcover visibility pressure
].freeze
HOUR_ELEMENTS =

Hourly elements — excludes day-only fields (tempmax/min, feelslikemax/min,
precipcover, sunrise/sunset) and adds solar data available at hourly resolution.

%w[
  datetime temp feelslike humidity dew
  precip precipprob preciptype snow snowdepth
  windspeed windgust winddir
  conditions source uvindex cloudcover visibility pressure
  solarradiation solarenergy
].freeze
CURRENT_ELEMENTS =

Current-conditions fields. The API always returns these as current snapshot.

%w[
  datetime temp feelslike humidity dew
  precip preciptype snow snowdepth
  windspeed windgust winddir
  conditions uvindex cloudcover visibility pressure
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(api_key: nil) ⇒ VisualCrossingClient

Returns a new instance of VisualCrossingClient.

Raises:

  • (ArgumentError)


65
66
67
68
69
70
# File 'app/services/weather/visual_crossing_client.rb', line 65

def initialize(api_key: nil)
  @api_key = api_key || Heatwave::Configuration.fetch(:visual_crossing_weather, :api_key)
  raise ArgumentError, 'Visual Crossing API key not configured' if @api_key.blank?

  @connection = build_connection
end

Instance Method Details

#get_forecast_for(latitude, longitude, datetime) ⇒ Hash?

Fetch the forecast for a single date at a lat/lng coordinate.

Used by AverageMonthlyTemperature to build monthly averages; returns the
raw day hash (string-keyed) so callers can read fields like "temp",
"tempmax", and "tempmin" directly without unwrapping a Response object.

Parameters:

  • latitude (Numeric)

    decimal degrees

  • longitude (Numeric)

    decimal degrees

  • datetime (Time, Date)

    the target date

Returns:

  • (Hash, nil)

    day hash on success, nil on API error



120
121
122
123
124
125
126
# File 'app/services/weather/visual_crossing_client.rb', line 120

def get_forecast_for(latitude, longitude, datetime)
  date = datetime.to_date.to_s
  result = lookup(location: "#{latitude}, #{longitude}", start_date: date, end_date: date)
  return nil unless result.success?

  result.data&.dig('days')&.first
end

#lookup(location:, start_date: nil, end_date: nil, unit_group: 'us', include_hours: false) ⇒ Response

Fetch weather data for a location and optional date range.

Parameters:

  • location (String)

    address, city name, postal/ZIP code, or "lat,lng"

  • start_date (String, nil) (defaults to: nil)

    YYYY-MM-DD, or dynamic keyword: "today", "yesterday",
    "last7days", "last30days", "lastyear". When nil → 15-day forecast from today.

  • end_date (String, nil) (defaults to: nil)

    YYYY-MM-DD. Only valid when start_date is also given.

  • unit_group (String) (defaults to: 'us')

    "us" (°F, mph, in) | "metric" (°C, km/h, mm) |
    "uk" (°C, mph, mm). Default: "us".

  • include_hours (Boolean) (defaults to: false)

    include hourly breakdown per day. Default: false.

Returns:

  • (Response)

    with :data hash (string-keyed) or :error string



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/services/weather/visual_crossing_client.rb', line 82

def lookup(location:, start_date: nil, end_date: nil, unit_group: 'us', include_hours: false)
  unit_group = UNIT_GROUPS.include?(unit_group) ? unit_group : 'us'

  path     = build_path(location, start_date, end_date)
  sections = include_hours ? 'days,hours,current' : 'days,current'
  elements = include_hours ? (DAY_ELEMENTS + HOUR_ELEMENTS).uniq.join(',') : DAY_ELEMENTS.join(',')

  response = @connection.get(path) do |req|
    req.params['key']         = @api_key
    req.params['unitGroup']   = unit_group
    req.params['contentType'] = 'json'
    req.params['include']     = sections
    req.params['elements']    = elements
    req.params['options']     = 'nonulls'  # strip nulls server-side to reduce payload
  end

  if response.success?
    Response.new(success?: true, data: format_response(response.body, unit_group), error: nil)
  else
    error_msg = extract_error(response)
    Rails.logger.warn("[VisualCrossingClient] API error #{response.status}: #{error_msg}")
    Response.new(success?: false, data: nil, error: error_msg)
  end
rescue Faraday::Error => e
  Rails.logger.error("[VisualCrossingClient] Connection error: #{e.message}")
  Response.new(success?: false, data: nil, error: "Connection error: #{e.message}")
end