1use crate::daily::{DailyForecast, DailyResponse};
2use crate::hourly::{HourlyForecast, HourlyResponse};
3use crate::location::{Location, LocationData, LocationResponse, SearchResponse, SearchResult};
4use crate::observation::{
5 Observation, ObservationResponse, PastObservationData, PastObservationsResponse,
6};
7use crate::warning::{Warning, WarningResponse};
8use crate::weather::{Weather, WeatherOptions};
9use anyhow::anyhow;
10use anyhow::Result;
11use chrono::Duration;
12use chrono::Utc;
13use serde::{Deserialize, Serialize};
14use serde_with::DurationSeconds;
15use std::collections::VecDeque;
16use std::thread::sleep;
17use tracing::{debug, error, trace};
18use ureq::{Agent, AgentBuilder, Error, Response};
19
20const URL_BASE: &str = "https://api.weather.bom.gov.au/v1/locations";
21const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) \
22 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
23
24#[serde_with::serde_as]
25#[derive(Clone, Debug, Deserialize, Serialize)]
26pub struct ClientOptions {
27 pub retry_limit: u64,
28 #[serde_as(as = "DurationSeconds<i64>")]
29 pub retry_delay: Duration,
30 #[serde_as(as = "DurationSeconds<i64>")]
31 pub timeout: Duration,
32 #[serde_as(as = "DurationSeconds<i64>")]
33 pub timeout_connect: Duration,
34 pub user_agent: String,
35}
36
37impl Default for ClientOptions {
38 fn default() -> Self {
39 Self {
40 retry_limit: 5,
41 retry_delay: Duration::seconds(7),
42 timeout: Duration::seconds(7),
43 timeout_connect: Duration::seconds(30),
44 user_agent: USER_AGENT.to_string(),
45 }
46 }
47}
48
49#[derive(Debug)]
50pub struct Client {
51 client: Agent,
52 opts: ClientOptions,
53}
54
55impl Client {
56 pub fn new(opts: ClientOptions) -> Client {
57 let client = AgentBuilder::new()
58 .timeout_read(opts.timeout.to_std().unwrap())
59 .timeout_write(opts.timeout.to_std().unwrap())
60 .timeout_connect(opts.timeout_connect.to_std().unwrap())
61 .user_agent(&opts.user_agent)
62 .build();
63 Client { client, opts }
64 }
65
66 fn get(&self, url: &str) -> Result<Response> {
67 debug!("Fetching {url}");
68 let mut attemps = 0;
69 while attemps < self.opts.retry_limit {
70 let mut retry_delay = self.opts.retry_delay;
71 match self.client.get(url).call() {
72 Ok(response) => {
73 return Ok(response);
74 }
75 Err(Error::Status(code, response)) => match code {
76 503 | 429 | 408 => {
77 if let Some(header) = response.header("retry-after") {
78 retry_delay = Duration::seconds(header.parse()?);
79 }
80 error!("{} for {}", code, url);
81 attemps += 1;
82 }
83 _ => {
84 let error = response.into_string()?;
85 error!("{code}: {error}");
86 return Err(anyhow!("{error}"));
87 }
88 },
89 Err(err) => {
90 let message = err.into_transport().unwrap().to_string();
91 error!("{message}");
92 attemps += 1;
93 }
94 }
95 debug!("Retrying in {} seconds", retry_delay.num_seconds());
96 sleep(retry_delay.to_std()?);
97 }
98 Err(anyhow!("Retry limit exceeded"))
99 }
100
101 fn get_string(&self, url: &str) -> Result<String> {
102 let response = self.get(url)?;
103 let string = response.into_string()?;
104 trace!("{}", &string);
105 Ok(string)
106 }
107
108 fn get_json(&self, url: &str) -> Result<serde_json::Value> {
109 let string = self.get_string(url)?;
110 let value = match serde_json::from_str(&string) {
111 Ok(json) => json,
112 Err(e) => {
113 debug!("{:?}", &string);
114 return Err(anyhow!("Unable to decode JSON. {e}"));
115 }
116 };
117 Ok(value)
118 }
119
120 pub fn search(&self, term: &str) -> Result<Vec<SearchResult>> {
121 let url = format!("{URL_BASE}?search={term}");
122 let response: SearchResponse = serde_json::from_value(self.get_json(&url)?)?;
123 debug!(
124 "Search term {} returned {} results.",
125 term,
126 response.data.len()
127 );
128 for result in &response.data {
129 debug!("{:#?}", result);
130 }
131 Ok(response.data)
132 }
133
134 pub fn get_observation(&self, geohash: &str) -> Result<Option<Observation>> {
136 let url = format!("{URL_BASE}/{}/observations", &geohash[..6]);
137 let response: ObservationResponse = serde_json::from_value(self.get_json(&url)?)?;
138 Ok(response.into())
139 }
140
141 pub fn get_daily(&self, geohash: &str) -> Result<DailyForecast> {
142 let url = format!("{URL_BASE}/{}/forecasts/daily", &geohash[..6]);
143 let response: DailyResponse = serde_json::from_value(self.get_json(&url)?)?;
144 Ok(response.into())
145 }
146
147 pub fn get_hourly(&self, geohash: &str) -> Result<HourlyForecast> {
148 let url = format!("{URL_BASE}/{}/forecasts/hourly", &geohash[..6]);
149 let response: HourlyResponse = serde_json::from_value(self.get_json(&url)?)?;
150 Ok(response.into())
151 }
152
153 pub fn get_warnings(&self, geohash: &str) -> Result<Vec<Warning>> {
154 let url = format!("{URL_BASE}/{}/warnings", &geohash[..6]);
155 let response: WarningResponse = serde_json::from_value(self.get_json(&url)?)?;
156 Ok(response.data)
157 }
158
159 pub fn get_weather(&self, geohash: &str) -> Result<Weather> {
160 let now = Utc::now();
161 let opts = WeatherOptions::default();
162
163 let daily_forecast = self.get_daily(geohash)?;
164 let mut next_daily_due = if let Some(next) = daily_forecast.next_issue_time {
165 next + opts.update_delay
166 } else {
167 daily_forecast.issue_time + opts.daily_update_frequency + opts.update_delay
168 };
169 if now > next_daily_due {
170 next_daily_due = now + opts.daily_overdue_delay;
171 }
172
173 let hourly_forecast = self.get_hourly(geohash)?;
174 let mut next_hourly_due =
175 hourly_forecast.issue_time + opts.hourly_update_frequency + opts.update_delay;
176 if now > next_hourly_due {
177 next_hourly_due = now + opts.hourly_overdue_delay;
178 }
179
180 let mut observations = VecDeque::new();
181 let mut next_observation_due = now + opts.observation_missing_delay;
182 if let Some(observation) = self.get_observation(geohash)? {
183 next_observation_due =
184 observation.issue_time + opts.observation_update_frequency + opts.update_delay;
185 if now > next_observation_due {
186 next_observation_due = now + opts.observation_overdue_delay;
187 }
188 observations.push_front(observation);
189 }
190
191 let warnings = self.get_warnings(geohash)?;
192 let next_warning_due = now + opts.warning_update_frequency;
193
194 Ok(Weather {
195 geohash: geohash.to_string(),
196 observations,
197 daily_forecast,
198 hourly_forecast,
199 warnings,
200 next_observation_due,
201 next_daily_due,
202 next_hourly_due,
203 next_warning_due,
204 opts,
205 })
206 }
207
208 pub fn get_location(&self, geohash: &str) -> Result<LocationData> {
209 let url = format!("{URL_BASE}/{}", &geohash[..6]);
210 let response: LocationResponse = serde_json::from_value(self.get_json(&url)?)?;
211 Ok(response.data)
212 }
213
214 pub fn get_past_observations(&self, location: &Location) -> Result<Vec<PastObservationData>> {
215 let Some(station) = &location.station else {
216 return Err(anyhow!("{} doesn't have a weather station", location.id));
217 };
218 let Some(wmo_id) = station.wmo_id else {
219 return Err(anyhow!("{} doesn't have a WMO ID", station.name));
220 };
221 let code = location.state.get_product_code("60910");
222 let url = format!("https://reg.bom.gov.au/fwo/{code}/{code}.{wmo_id}.json");
223 let response: PastObservationsResponse = serde_json::from_value(self.get_json(&url)?)?;
224 Ok(response.observations.data)
225 }
226
227 pub fn get_station_list(&self) -> Result<String> {
228 let url = "https://reg.bom.gov.au/climate/data/lists_by_element/stations.txt";
229 self.get_string(url)
230 }
231}
232
233impl Default for Client {
234 fn default() -> Self {
235 let opts = ClientOptions::default();
236 Self::new(opts)
237 }
238}