Class: SmoEaHydrology::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/smo_ea_hydrology/client.rb

Constant Summary collapse

BASE_URL =
"https://environment.data.gov.uk/hydrology"
FM_BASE_URL =
"https://environment.data.gov.uk/flood-monitoring"

Instance Method Summary collapse

Instance Method Details

#batch_download(from:, to:, output_dir:, refs: nil) ⇒ Hash

Downloads readings for multiple stations to individual CSV files.

Parameters:

  • from (String, Date)

    start date inclusive (YYYY-MM-DD)

  • to (String, Date)

    end date inclusive (YYYY-MM-DD)

  • output_dir (String)

    directory to write CSV files into

  • refs (Array<String>, nil) (defaults to: nil)

    station references to download; nil downloads all active 15-min stations

Returns:

  • (Hash)

    { station_reference => { path:, count: } }



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/smo_ea_hydrology/client.rb', line 155

def batch_download(from:, to:, output_dir:, refs: nil)
  require "csv"
  require "fileutils"
  FileUtils.mkdir_p(output_dir)

  stations = rainfall_15min_stations
  stations = stations.select { |s| refs.map(&:to_s).include?(s.station_reference.to_s) } if refs

  results = {}
  stations.each_with_index do |station, i|
    ref  = station.station_reference.to_s
    path = File.join(output_dir, "#{ref}_#{parse_date(from)}_#{parse_date(to)}.csv")
    $stdout.print "\r  [#{i + 1}/#{stations.size}] #{ref}    "
    $stdout.flush

    count = readings_to_csv(station_reference: ref, from: from, to: to, path: path)
    results[ref] = { path: path, count: count }
  rescue => e
    results[ref] = { path: path, count: 0, error: e.message }
  end

  $stdout.puts
  results
end

#find_stations(query) ⇒ Array<Station>

Search stations by partial name (case-insensitive) or exact station reference. Calls rainfall_15min_stations internally so no coverage dates are included.

Parameters:

  • query (String)

    partial station name or exact reference

Returns:



94
95
96
97
98
99
100
# File 'lib/smo_ea_hydrology/client.rb', line 94

def find_stations(query)
  q = query.to_s.strip.downcase
  rainfall_15min_stations.select do |s|
    s.station_reference.to_s.downcase == q ||
      s.label.to_s.downcase.include?(q)
  end
end

#measures(station_reference) ⇒ Array<Measure>

Returns all 15-min rainfall measures for a given station.

Parameters:

  • station_reference (String)

    e.g. “589359”

Returns:



45
46
47
48
49
50
# File 'lib/smo_ea_hydrology/client.rb', line 45

def measures(station_reference)
  items = get("/id/measures", observedProperty: "rainfall",
                             periodName:        "15min",
                             "station.stationReference": station_reference.to_s)
  items.map { |item| parse_measure(item) }
end

#rainfall_15min_inventoryArray<InventoryEntry>

Returns a combined inventory of all active 15-min rainfall stations with their measures and coverage dates (earliest and latest reading timestamps).

Note: this makes two additional API calls per station (one to the hydrology API for the earliest reading, one to the flood-monitoring API for the latest), so it is slow for large inventories. Progress is printed to $stdout.

Returns:



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/smo_ea_hydrology/client.rb', line 188

def rainfall_15min_inventory
  stations = rainfall_15min_stations
  entries  = []

  stations.each_with_index do |station, i|
    $stdout.print "\r  #{i + 1}/#{stations.size} #{station.station_reference}    "
    $stdout.flush

    measures_list = measures(station.station_reference)
    next if measures_list.empty?

    measure = measures_list.first
    from_t  = fetch_earliest(measure.id)
    to_t    = fetch_latest_fm(station.station_reference)

    entries << InventoryEntry.new(
      station_reference: station.station_reference,
      station_label:     station.label,
      lat:               station.lat,
      long:              station.long,
      easting:           station.easting,
      northing:          station.northing,
      date_opened:       station.date_opened,
      measure_id:        measure.id,
      period_name:       measure.period_name,
      unit_name:         measure.unit_name,
      value_type:        measure.value_type,
      coverage_from:     from_t,
      coverage_to:       to_t
    )
  end

  $stdout.puts
  entries
end

#rainfall_15min_inventory_to_csv(path) ⇒ Object

Writes the full 15-min rainfall inventory to a CSV file. Includes coverage dates — makes 2 extra API calls per station.

Parameters:

  • path (String)

    output file path



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/smo_ea_hydrology/client.rb', line 128

def rainfall_15min_inventory_to_csv(path)
  require "csv"
  entries = rainfall_15min_inventory
  CSV.open(path, "w") do |csv|
    csv << %w[station_reference station_label lat long easting northing
              date_opened measure_id period_name unit_name value_type
              coverage_from coverage_to]
    entries.each do |e|
      csv << [
        e.station_reference, e.station_label, e.lat, e.long,
        e.easting, e.northing, e.date_opened, e.measure_id,
        e.period_name, e.unit_name, e.value_type,
        e.coverage_from_s, e.coverage_to_s
      ]
    end
  end
  entries.size
end

#rainfall_15min_stationsArray<Station>

Returns all active stations that have at least one 15-min rainfall measure.

Returns:



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/smo_ea_hydrology/client.rb', line 17

def rainfall_15min_stations
  items = paginate("/id/stations", observedProperty: "rainfall", "status.label": "Active")
  items.filter_map do |item|
    measures   = Array(item["measures"])
    measure_15 = measures.find { |m| m.is_a?(Hash) && m["period"] == 900 }
    next unless measure_15

    Station.new(
      id:                item["@id"],
      label:             item["label"],
      station_reference: item["stationReference"] || item["notation"],
      wiski_id:          item["wiskiID"],
      easting:           item["easting"],
      northing:          item["northing"],
      lat:               item["lat"],
      long:              item["long"],
      date_opened:       item["dateOpened"],
      status:            label_of(item["status"]),
      measure_id:        measure_15["@id"],
      measure_label:     derive_measure_label(measure_15)
    )
  end
end

#rainfall_15min_stations_with_coverageArray<Station>

Like rainfall_15min_stations but also fetches coverage_from / coverage_to for every station. Makes 2 extra API calls per station — slow for all 900+. Progress is printed to $stdout.

Returns:



77
78
79
80
81
82
83
84
85
86
87
# File 'lib/smo_ea_hydrology/client.rb', line 77

def rainfall_15min_stations_with_coverage
  stations = rainfall_15min_stations
  stations.each_with_index do |station, i|
    $stdout.print "\r  #{i + 1}/#{stations.size} #{station.station_reference}    "
    $stdout.flush
    station.coverage_from = fetch_earliest(station.measure_id)
    station.coverage_to   = fetch_latest_fm(station.station_reference)
  end
  $stdout.puts
  stations
end

#readings(measure_id, from:, to:) ⇒ Array<Reading>

Returns 15-min rainfall readings for a measure over a date/datetime range.

Parameters:

  • measure_id (String)

    full measure URI or the path-only ID

  • from (String, Date, Time)

    start inclusive — “YYYY-MM-DD” or “YYYY-MM-DD HH:MM”

  • to (String, Date, Time)

    end inclusive — “YYYY-MM-DD” or “YYYY-MM-DD HH:MM”

Returns:



58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/smo_ea_hydrology/client.rb', line 58

def readings(measure_id, from:, to:)
  id_path    = measure_path(measure_id)
  from_str   = parse_datetime(from)
  to_str     = parse_datetime(to)
  date_only  = !from_str.include?("T")
  params     = if date_only
    { "mineq-date": from_str, "maxeq-date": to_str }
  else
    { "mineq-dateTime": from_str, "maxeq-dateTime": to_str }
  end
  items = paginate("#{id_path}/readings", **params)
  items.map { |item| parse_reading(item) }
end

#readings_to_csv(station_reference:, from:, to:, path:) ⇒ Integer

Downloads 15-min readings for a single station to a CSV file.

Parameters:

  • station_reference (String)
  • from (String, Date)

    start date inclusive (YYYY-MM-DD)

  • to (String, Date)

    end date inclusive (YYYY-MM-DD)

  • path (String)

    output file path

Returns:

  • (Integer)

    number of readings written

Raises:



109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/smo_ea_hydrology/client.rb', line 109

def readings_to_csv(station_reference:, from:, to:, path:)
  require "csv"
  ms = measures(station_reference)
  raise ApiError, "No 15-min rainfall measure found for #{station_reference}" if ms.empty?

  rows = readings(ms.first.id, from: from, to: to)
  CSV.open(path, "w") do |csv|
    csv << %w[datetime value_mm quality completeness]
    rows.each do |r|
      csv << [r.datetime.strftime("%Y-%m-%d %H:%M:%S"), r.value, r.quality, r.completeness]
    end
  end
  rows.size
end