1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
extern crate chrono;
extern crate i3status_ext;
extern crate notify_rust;
extern crate openweathermap;
//#[macro_use]
extern crate clap;
use clap::Parser;
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
mod level;
mod notify;
mod spot;
mod weather;
use level::Level;
use notify::Notify;
use spot::*;
use weather::*;
#[cfg(test)]
mod tests;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
/// Location city name, city ID or coordinate
///
/// City's name maybe followed by comma-separated 2-letter (state code for the USA locations and) country code (ISO3166) or city ID (see https://openweathermap.org/find) or geographical coordinate as comma-separated latitude and longitude.
#[clap(short='c', long, value_parser, default_value_t = String::from("Berlin,DE"))]
location: String,
/// OpenWeatherMap API key (see at https://openweathermap.org/api)
#[clap(short = 'k', long, value_parser)]
apikey: String,
/// Display format string
///
/// Format string including one ore more of the following keys
///
/// {city} City name
///
/// {main} Group of weather parameters (Rain, Snow, Extreme etc.)
///
/// {description} Weather condition within the group
///
/// {icon} Weather icon
///
/// {pressure} Atmospheric pressure (on the sea level, if there is no sea_level or grnd_level data), hPa
///
/// {humidity} Humidity, %
///
/// {wind} Wind direction as N, NW, W, SW, S, SO, O or NO
///
/// {wind_icon} Wind direction as arrow icon
///
/// {wind_speed} Wind speed, {speed_unit}
///
/// {wind_deg} Wind direction, degrees (meteorological)
///
/// {deg_unit} Direction unit (degrees: °)
///
/// {visibility} Visibility, meter
///
/// {visibility_km} Visibility, kilometer
///
/// {rain.1h} Rain volume for the last 1 hour, mm
///
/// {rain.3h} Rain volume for the last 3 hours, mm
///
/// {snow.1h} Snow volume for the last 1 hour, mm
///
/// {snow.3h} Snow volume for the last 3 hours, mm
///
/// {temp_min} Minimum temperature at the moment. This is minimal currently observed temperature (within large megalopolises and urban areas), {temp_unit}
///
/// {temp_max} Maximum temperature at the moment. This is maximal currently observed temperature (within large megalopolises and urban areas), {temp_unit}
///
/// {feels_like} Temperature. This temperature parameter accounts for the human perception of weather, {temp_unit}
///
/// {temp} Temperature, {temp_unit}
///
/// {temp_unit} Temperature (standard=K, metric=°C, imperial=°F)
///
/// {speed_unit} Wind speed unit
/// (standard=m/s, metric=m/s, imperial=mi/h)
///
/// {update} Local time of last update, HH:MM
///
/// {iss} ISS spotting time (HH:MM) or latency (-hh::mm::ss) or duration (+hh::mm::ss)
///
/// {iss_icon} show 🛰 if ISS is visible
///
/// {iss_space} space (' ') if any ISS information is displayed
#[clap(short, long, value_parser, default_value_t=String::from("{city} {icon} {temp}{temp_unit}"))]
format: String,
/// Position of output in JSON when wrapping i3status
#[clap(short, long, value_parser, default_value_t = 0)]
position: usize,
/// Two character language code of weather descriptions
#[clap(short, long, value_parser, default_value_t = String::from("en"))]
lang: String,
/// Reverse position (from right)
#[clap(short, long, action)]
reverse: bool,
/// Use imperial units
#[clap(short, long, value_parser, default_value_t = String::from("metric"))]
units: String,
/// Duration of polling period in minutes
#[clap(short = 'P', long, value_parser, default_value_t = 10)]
poll: u64,
/// Duration in minutes when ISS rising is "soon" in minutes
#[clap(short, long, value_parser, default_value_t = 15)]
soon: i64,
/// Maximum cloudiness in percent at which ISS can be treated as visible
#[clap(short = 'C', long = "cloudiness", value_parser, default_value_t = 25)]
max_cloudiness: u64,
/// Show ISS spotting events when they are at daytime
#[clap(short = 'D', long = "daytime", action)]
dayspot: bool,
/// ISS spotting level
///
/// watch = only show duration while ISS is visible
/// soon = show latency until ISS will be visible (includes 'watch')
/// rise = show time of next spotting event (includes 'soon' and 'watch')
/// far = show prediction time in days if no prediction available
#[clap(short='L', long, value_enum, default_value_t = Level::SOON)]
level: Level,
/// Let ISS icon blink when visible
#[clap(short, long, action)]
blink: bool,
/// Show notifications about ISS getting visible
#[clap(short, long, action)]
notify: bool,
/// Do not process i3status from stdin, instead show formatted string
#[clap(short, long, action)]
test: bool,
/// Number of ISS spottings that will be fetched from open-notify.org
#[clap(short = 'T', long, value_parser, default_value_t = 100)]
prevision: u8,
}
/// continuously inject weather into incoming json lines from i3status and pass through
fn main() {
// fetch arguments
let args = Args::parse();
// start our observatory via OWM
let owm = &openweathermap::init(
&args.location,
&args.units,
&args.lang,
&args.apikey,
args.poll,
);
// open-notify receiver will get created if we get coordinates from weather update
let mut iss: Option<open_notify::Receiver> = None;
// start i3status parsing
let mut io = match args.test {
false => i3status_ext::begin().unwrap(),
true => i3status_ext::begin_dummy().unwrap(),
};
// we may override format for error messages
let mut format_str = openweathermap::LOADING.to_string();
// remember visibility from weather report for ISS spotting
let mut visible: bool = false;
// remember daytime from weather report for ISS spotting
let mut daytime: DayTime;
let mut dt: Option<&DayTime> = None;
// remember duration of current spotting event in milliseconds for motification timeout
let mut duration = Duration::from_millis(0);
// state of current notification
let mut notify = Notify::new(args.notify);
// create blinking flag
let mut blinking: bool = false;
// latest spotting update
let mut spottings: Vec<open_notify::Spot> = Vec::new();
// all fetched information
let mut props: HashMap<&str, String> = new_properties();
loop {
// update current weather info if there is an update available
match openweathermap::update(owm) {
Some(response) => match response {
Ok(w) => {
// remember cloudiness for spotting visibility
visible = w.clouds.all <= args.max_cloudiness as f64;
// remember daytime from current weather if wanted
if !args.dayspot {
daytime = DayTime::from_utc(w.sys.sunrise, w.sys.sunset);
dt = Some(&daytime);
}
// check if we have to start open_notify thread
if iss.is_none() && args.format.contains("{iss_") {
iss = Some(open_notify::init(
w.coord.lat,
w.coord.lon,
0.0,
args.prevision,
90,
));
}
// get weather properties
get_weather(&mut props, &w, &args.units);
// reset format string
format_str = args.format.to_string();
}
Err(e) => format_str = e,
},
None => (),
}
match iss {
Some(ref iss) => match open_notify::update(iss) {
Some(response) => match response {
Ok(s) => {
// remember duration of current spotting event in milliseconds for motification timeout
duration = match open_notify::find_current(&s, dt, chrono::Local::now()) {
Some(s) => Duration::from_millis(s.duration.num_milliseconds() as u64),
None => Duration::from_millis(0),
};
// rememeber current spotting events
spottings = s;
// reset format string
format_str = args.format.to_string();
}
Err(e) => {
// do not show "loading..." twice
if e != openweathermap::LOADING {
format_str = e
}
}
},
None => (),
},
None => (),
}
// continuously get spot properties
let level = get_spots(
&mut props,
&spottings,
args.soon,
visible,
dt,
blinking,
&args.level,
);
// check if we shall generate a notification
notify.notification(duration, level);
// toggle blinking flag
if args.blink {
blinking = !blinking;
}
let output = format_string(&format_str, &props);
if !args.test {
// insert current properties and print json string or original line
i3status_ext::update(&mut io, "i3owm", args.position, args.reverse, &output).unwrap();
} else {
println!("{}", output);
thread::sleep(Duration::from_secs(1));
}
}
}
/// insert properties into format string
/// #### Parameters
/// - `format`: output format (string including some of the available keys)
/// - `props`: property map to get data to insert from
/// #### Return value
/// - formatted string
fn format_string(format: &str, props: &HashMap<&str, String>) -> String {
let mut result: String = format.to_string();
let mut iss: bool = false;
// replace all keys by their values
for (k, v) in props {
let r = result.replace(k, v);
if r != result {
result = r;
// tests if an '{iss_' key of value was inserted
iss = iss || (k.contains("{iss_") && v != "");
}
}
// insert space at '{iss_space}' if we inserted '{iss_' keys of value
return result.replace(
"{iss_space}",
match iss {
true => " ",
false => "",
},
);
}