Merge lp:~jacob1237/gazette/weatherapi into lp:gazette
- weatherapi
- Merge into trunk
Status: | Needs review |
---|---|
Proposed branch: | lp:~jacob1237/gazette/weatherapi |
Merge into: | lp:gazette |
Diff against target: |
1006 lines (+697/-220) 7 files modified
CMakeLists.txt (+6/-3) src/Plugs/Config.vala.cmake (+0/-27) src/Services/Weather.vala (+176/-190) src/Services/WeatherData.vala (+241/-0) src/Utils/CMakeLists.txt (+63/-0) src/Utils/Yahoo.vala (+197/-0) src/Utils/YahooTest (+14/-0) |
To merge this branch: | bzr merge lp:~jacob1237/gazette/weatherapi |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Eduard Gotwig | Pending | ||
Review via email: mp+314773@code.launchpad.net |
This proposal supersedes a proposal from 2017-01-14.
Commit message
Description of the change
Hello!
I'd like to contribute some urgent fixes to the project.
Yahoo Weather API is totally broken since they locked their old URL (http://
So my fix is using https:/
Also I made some restructuring of the Weather Service.
Here is a list of all modifications:
1. Fixed Yahoo weather API
2. Weather fetching process has been decoupled from the main Weather Service - now it calls external command to get the weather data
3. Added additional environment variable CUSTOM_WEATHER_CMD to be able to run custom user scripts (later I will add this field to the Switchboard plugin, right now it is just for Devs)
4. CMakeLists.txt has been adapted to the new architecture
Right now the Weather Service is calling Process.
The pipe data interface is very simple:
Weather Service calls external command with <unit> and <woeid> (Yahoo Geo code) parameters like this:
weather-yahoo f 2108210
and takes the result in the following format (with line breaks):
24 -5
24 2 -5 -10
23 3 -6 -15
where the first line is the current weather and subsequent lines are the forecasts in format <condition code> <day of week (0-7)> <temp low> <temp high>
The package was tested on Elementary OS Luna 32bit only (cmake && make install).
Please review the changes and share your opinion on that branch.
P.S. I have some plans to make the same architecture for RSS (with external command). Will try to prepare some blueprints about that.
Preview Diff
1 | === modified file 'CMakeLists.txt' |
2 | --- CMakeLists.txt 2013-04-20 16:58:56 +0000 |
3 | +++ CMakeLists.txt 2017-01-14 20:16:28 +0000 |
4 | @@ -14,9 +14,10 @@ |
5 | |
6 | set (DATADIR "${CMAKE_INSTALL_PREFIX}/share") |
7 | set (PKGDATADIR "${DATADIR}/${NAME}") |
8 | +set (PLUGINDIR "${PKGDATADIR}/plugins") |
9 | set (GETTEXT_PACKAGE "${NAME}") |
10 | set (RELEASE_NAME "Simple and functional.") |
11 | -set (VERSION "0.1") |
12 | +set (VERSION "0.2") |
13 | set (VERSION_INFO "Release") |
14 | set (CMAKE_C_FLAGS "-ggdb") |
15 | set (PREFIX ${CMAKE_INSTALL_PREFIX}) |
16 | @@ -28,7 +29,7 @@ |
17 | add_definitions(-DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\") |
18 | |
19 | find_package(PkgConfig) |
20 | -pkg_check_modules(DEPS REQUIRED goa-1.0 libgdata libsoup-2.4 pantheon granite clutter-gtk-1.0 zeitgeist-1.0) |
21 | +pkg_check_modules(DEPS REQUIRED goa-1.0 libgdata libsoup-2.4 libxml-2.0 pantheon granite clutter-gtk-1.0 zeitgeist-1.0) |
22 | |
23 | link_libraries(${DEPS_LIBRARIES}) |
24 | link_directories(${DEPS_LIBRARY_DIRS}) |
25 | @@ -45,9 +46,10 @@ |
26 | src/Services/Files.vala |
27 | src/Services/Service.vala |
28 | src/Services/News.vala |
29 | + src/Services/WeatherData.vala |
30 | src/Services/Weather.vala |
31 | src/Services/Calendar.vala |
32 | - src/Widgets/GazetteWindow.vala |
33 | + src/Widgets/GazetteWindow.vala |
34 | src/Widgets/ShadowedLabel.vala |
35 | ${CMAKE_BINARY_DIR}/src/Config.vala |
36 | PACKAGES |
37 | @@ -66,6 +68,7 @@ |
38 | |
39 | add_subdirectory (po) |
40 | add_subdirectory (src/Plugs) |
41 | +add_subdirectory (src/Utils) |
42 | |
43 | include(GSettings) |
44 | add_schema ("data/org.pantheon.gazette.gschema.xml") |
45 | |
46 | === removed file 'src/Plugs/Config.vala.cmake' |
47 | --- src/Plugs/Config.vala.cmake 2013-04-20 17:18:50 +0000 |
48 | +++ src/Plugs/Config.vala.cmake 1970-01-01 00:00:00 +0000 |
49 | @@ -1,27 +0,0 @@ |
50 | -// |
51 | -// Copyright (C) 2011 Tom Beckmann |
52 | -// |
53 | -// This program is free software: you can redistribute it and/or modify |
54 | -// it under the terms of the GNU General Public License as published by |
55 | -// the Free Software Foundation, either version 3 of the License, or |
56 | -// (at your option) any later version. |
57 | -// |
58 | -// This program is distributed in the hope that it will be useful, |
59 | -// but WITHOUT ANY WARRANTY; without even the implied warranty of |
60 | -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
61 | -// GNU General Public License for more details. |
62 | -// |
63 | -// You should have received a copy of the GNU General Public License |
64 | -// along with this program. If not, see <http://www.gnu.org/licenses/>. |
65 | -// |
66 | - |
67 | -namespace Constants { |
68 | - public const string DATADIR = "@DATADIR@"; |
69 | - public const string PKGDATADIR = "@PKGDATADIR@"; |
70 | - public const string GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@"; |
71 | - public const string RELEASE_NAME = "@RELEASE_NAME@"; |
72 | - public const string VERSION = "@VERSION@"; |
73 | - public const string VERSION_INFO = "@VERSION_INFO@"; |
74 | - public const string PLUGINDIR = "@PLUGINDIR@"; |
75 | - public const string SCHEMA = "org.pantheon.gazette"; |
76 | -} |
77 | |
78 | === modified file 'src/Services/Weather.vala' |
79 | --- src/Services/Weather.vala 2013-07-29 10:34:55 +0000 |
80 | +++ src/Services/Weather.vala 2017-01-14 20:16:28 +0000 |
81 | @@ -1,213 +1,199 @@ |
82 | - |
83 | -const string [] condition_codes = { |
84 | - "c", // tornado |
85 | - "0", // tropical storm |
86 | - "c", // hurricane |
87 | - "1", // severe thunderstorms |
88 | - "1", // thunderstorms |
89 | - "2", // mixed rain and snow |
90 | - "3", // mixed rain and sleet |
91 | - "2", // mixed snow and sleet |
92 | - "e", // freezing drizzle |
93 | - "3", // drizzle |
94 | - "7", // freezing rain |
95 | - "3", // showers |
96 | - "3", // showers |
97 | - "6", // snow flurries |
98 | - "G", // light snow showers G |
99 | - "#", // blowing snow # |
100 | - "h", // snow " |
101 | - "7", // hail |
102 | - "7", // sleet |
103 | - "f", // dust |
104 | - "f", // foggy |
105 | - "f", // haze |
106 | - "M", // smoky M |
107 | - "c", // blustery |
108 | - "S", // windy S |
109 | - "h", // cold |
110 | - "N", // cloudy N |
111 | - "4", // mostly cloudy (night) 4 |
112 | - "3", // mostly cloudy (day) 3 |
113 | - "K", // partly cloudy (night) K |
114 | - "J", // partly cloudy (day) J |
115 | - "C", // clear (night) C |
116 | - "B", // sunny B |
117 | - "2", // fair (night) 2 |
118 | - "B", // fair (day) B |
119 | - "X", // mixed rain and hail X |
120 | - "1", // hot 1 |
121 | - "1", // isolated thunderstorms |
122 | - "1", // scattered thunderstorms |
123 | - "1", // scattered thunderstorms |
124 | - "e", // scattered showers |
125 | - "#", // heavy snow # |
126 | - "2", // scattered snow showers |
127 | - "#", // heavy snow # |
128 | - "H", // partly cloudy H |
129 | - "1", // thundershowers |
130 | - "2", // snow showers |
131 | - "1", // isolated thundershowers |
132 | - ")" // not available ) |
133 | -}; |
134 | +using WeatherData; |
135 | |
136 | public class Weather : Service |
137 | { |
138 | - string [] condition_texts = { |
139 | - _("Tornado"), // tornado |
140 | - _("Tropical storm"), // tropical storm |
141 | - _("Hurricane"), // hurricane |
142 | - _("Severe thunderstorms"), // severe thunderstorms |
143 | - _("Thunderstorms"), // thunderstorms |
144 | - _("Mixed rain and snow"), // mixed rain and snow |
145 | - _("Mixed rain and sleet"), // mixed rain and sleet |
146 | - _("Mixed snow and sleet"), // mixed snow and sleet |
147 | - _("Freezing drizzle"), // freezing drizzle |
148 | - _("Drizzle"), // drizzle |
149 | - _("Freezing rain"), // freezing rain |
150 | - _("Showers"), // showers |
151 | - _("Showers"), // showers |
152 | - _("Snow flurries"), // snow flurries |
153 | - _("Light snow showers"), // light snow showers G |
154 | - _("Blowing snow"), // blowing snow # |
155 | - _("Snow"), // snow " |
156 | - _("Hail"), // hail |
157 | - _("Sleet"), // sleet |
158 | - _("Dust"), // dust |
159 | - _("Foggy"), // foggy |
160 | - _("Haze"), // haze |
161 | - _("Smoky"), // smoky M |
162 | - _("Blustery"), // blustery |
163 | - _("Windy"), // windy S |
164 | - _("Cold"), // cold |
165 | - _("Cloudy"), // cloudy N |
166 | - _("Mostly cloudy"), // mostly cloudy (night) 4 |
167 | - _("Mostly cloudy"), // mostly cloudy (day) 3 |
168 | - _("Partly cloudy"), // partly cloudy (night) K |
169 | - _("Partly cloudy"), // partly cloudy (day) J |
170 | - _("Clear"), // clear (night) C |
171 | - _("Sunny"), // sunny B |
172 | - _("Fair"), // fair (night) 2 |
173 | - _("Fair"), // fair (day) B |
174 | - _("Mixed rain and hail"), // mixed rain and hail X |
175 | - _("Hot"), // hot 1 |
176 | - _("Isolated thunderstorms"), // isolated thunderstorms |
177 | - _("Scattered thunderstorms"), // scattered thunderstorms |
178 | - _("Scattered thunderstorms"), // scattered thunderstorms |
179 | - _("Scattered showers"), // scattered showers |
180 | - _("Heavy snow"), // heavy snow # |
181 | - _("Scattered snow showers"), // scattered snow showers |
182 | - _("Heavy snow"), // heavy snow # |
183 | - _("Partly cloudy"), // partly cloudy H |
184 | - _("Thundershowers"), // thundershowers |
185 | - _("Snow showers"), // snow showers |
186 | - _("Isolated thundershowers"), // isolated thundershowers |
187 | - _("Not available") // not available ) |
188 | - }; |
189 | - |
190 | - string [] day_texts = { |
191 | - _("Mon"), |
192 | - _("Tue"), |
193 | - _("Wed"), |
194 | - _("Thu"), |
195 | - _("Fri"), |
196 | - _("Sat"), |
197 | - _("Sun") |
198 | - }; |
199 | - |
200 | string unit; |
201 | - string text; |
202 | - Soup.SessionAsync session; |
203 | - Soup.Message message; |
204 | + |
205 | Settings settings; |
206 | + WeatherData.Weather weather; |
207 | + |
208 | ShadowedLabel label; |
209 | ShadowedLabel reload; |
210 | |
211 | + protected string cmd; |
212 | + protected const string default_cmd = "weather-yahoo"; |
213 | + |
214 | + /** |
215 | + * Constructor |
216 | + */ |
217 | public Weather () |
218 | { |
219 | base ("weather"); |
220 | + |
221 | + init_external_command(); |
222 | + |
223 | settings = new Settings ("org.pantheon.gazette.weather"); |
224 | settings.changed.connect( (key) => { update(); }); |
225 | + |
226 | label = new ShadowedLabel(""); |
227 | reload = get_reload_label(_("weather")); |
228 | reload.hide(); |
229 | - session = new Soup.SessionAsync (); |
230 | + |
231 | add_child (label); |
232 | add_child (reload); |
233 | + |
234 | load(); |
235 | - Timeout.add(settings.get_int("update-interval"), update); |
236 | - } |
237 | - |
238 | - public override void create () |
239 | - { |
240 | - |
241 | - } |
242 | - |
243 | - public string[] get_attributes (string data, string tagname, string [] attrs, int offset = 0) |
244 | - { |
245 | - var start = data.index_of ("<" + tagname, offset) + tagname.length + 1; |
246 | - var end = data.index_of ("/>", start); |
247 | - var tmp_data = data.substring (start, end - start); |
248 | - |
249 | - var res = new string[attrs.length]; |
250 | - for (var i = 0; i < attrs.length; i++) { |
251 | - res[i] = get_attribute_value (tmp_data, attrs[i]); |
252 | - } |
253 | - return res; |
254 | - } |
255 | - |
256 | - public string get_attribute_value (string data, string attr) |
257 | - { |
258 | - var start = data.index_of (attr + "=\"") + attr.length + 2; |
259 | - var end = data.index_of ("\"", start); |
260 | - |
261 | - return data.substring (start, end - start); |
262 | - } |
263 | - public override bool update() { |
264 | - debug ("Updating Weather"); |
265 | - reload.hide (); |
266 | - label.label = "<span face='Open Sans Light' font='16'>" + _("Loading weather,\nplease wait.") + "</span>"; |
267 | - label.show (); |
268 | - string id = settings.get_int ("weather-id").to_string(); |
269 | + |
270 | + Timeout.add(settings.get_int("update-interval"), update); |
271 | + } |
272 | + |
273 | + public override void create () {} |
274 | + |
275 | + /** |
276 | + * Initialize external command path |
277 | + */ |
278 | + protected void init_external_command() |
279 | + { |
280 | + var weather_cmd = Environment.get_variable("CUSTOM_WEATHER_CMD"); |
281 | + |
282 | + cmd = (weather_cmd != null) |
283 | + ? weather_cmd |
284 | + : Constants.PLUGINDIR + "/" + default_cmd; |
285 | + |
286 | + // Check command existence |
287 | + var f = File.new_for_path(cmd); |
288 | + |
289 | + if (!f.query_exists() || cmd.length == 0) { |
290 | + error("Unable to find weather utility. Please check your program installation."); |
291 | + } |
292 | + } |
293 | + |
294 | + /** |
295 | + * Show "reload" message |
296 | + */ |
297 | + protected void show_reload() |
298 | + { |
299 | + label.hide(); |
300 | + reload.show(); |
301 | + } |
302 | + |
303 | + /** |
304 | + * Draw weather data on label |
305 | + */ |
306 | + protected void draw_weather() |
307 | + { |
308 | + if (weather.forecasts.length < 2) { |
309 | + warning("Not enough forecasts for successful render"); |
310 | + return; |
311 | + } |
312 | + |
313 | + // Variables shortcuts |
314 | + unowned WeatherData.Weather w = weather; |
315 | + unowned WeatherData.Forecast f1 = w.forecasts.get(0); |
316 | + unowned WeatherData.Forecast f2 = w.forecasts.get(1); |
317 | + |
318 | + // Current day and unit |
319 | + var today = new DateTime.now_local().format(_("%A")); |
320 | + var unit_name = unit.up(); |
321 | + |
322 | + label.label = |
323 | + @"<span face='Open Sans Light' font='24'>$today</span>\n" + |
324 | + @"<span face='Open Sans Light' font='16'><i>$(w.condition)</i></span>\n" + |
325 | + @"<span face='gazetteweather' font='68'>$(w.icon)</span>" + |
326 | + @"<span face='Raleway' weight='100' font='72'> $(w.temp)</span>" + |
327 | + @"<span face='Raleway' weight='100' font='40'> ° $unit_name</span>\n" + |
328 | + |
329 | + // Forecast 1 |
330 | + @"<span face='gazetteweather' font='30'>$(f1.icon)</span>" + |
331 | + @"<span face='Open Sans Light' font='26'> $(f1.day_string) </span>" + |
332 | + |
333 | + // Forecast 2 |
334 | + @"<span face='gazetteweather' font='30'>$(f2.icon)</span>" + |
335 | + @"<span face='Open Sans Light' font='26'> $(f2.day_string)</span>\n" + |
336 | + |
337 | + @"<span face='Raleway' font='21'>$(f1.low) - $(f1.high)°$unit_name </span>" + |
338 | + @"<span face='Raleway' font='21'>$(f2.low) - $(f2.high)°$unit_name </span>"; |
339 | + } |
340 | + |
341 | + /** |
342 | + * Spawn external process and read weather data |
343 | + */ |
344 | + protected void update_weather(string unit, string geo_id) |
345 | + { |
346 | + Pid? child_pid; |
347 | + |
348 | + int standard_input; |
349 | + int? standard_output; |
350 | + int? standard_error; |
351 | + |
352 | + string[] spawn_env = Environ.get(); |
353 | + string[] spawn_args = {cmd, unit, geo_id}; |
354 | + |
355 | + try { |
356 | + bool result = Process.spawn_async_with_pipes("/", |
357 | + spawn_args, |
358 | + spawn_env, |
359 | + SpawnFlags.DO_NOT_REAP_CHILD, |
360 | + null, |
361 | + out child_pid, |
362 | + out standard_input, |
363 | + out standard_output, |
364 | + out standard_error); |
365 | + |
366 | + if (!result || child_pid == null || standard_output == null) { |
367 | + throw new SpawnError.FAILED("Unable to spawn weather utility process"); |
368 | + } |
369 | + } |
370 | + catch (SpawnError e) { |
371 | + critical(e.message); |
372 | + |
373 | + show_reload(); |
374 | + return; |
375 | + } |
376 | + |
377 | + // Wait for process termination |
378 | + ChildWatch.add(child_pid, (pid, status) => |
379 | + { |
380 | + string buf; |
381 | + size_t len; |
382 | + |
383 | + try { |
384 | + if (status == 0) { |
385 | + var output = new IOChannel.unix_new(standard_output); |
386 | + |
387 | + weather = WeatherData.parse(output); |
388 | + output.shutdown(false); |
389 | + |
390 | + draw_weather(); |
391 | + } |
392 | + else { |
393 | + var errors = new IOChannel.unix_new(standard_error); |
394 | + |
395 | + errors.read_to_end(out buf, out len); |
396 | + warning(buf); |
397 | + |
398 | + errors.shutdown(false); |
399 | + |
400 | + show_reload(); |
401 | + } |
402 | + } |
403 | + catch (Error e) { |
404 | + warning(e.message); |
405 | + } |
406 | + finally { |
407 | + Process.close_pid(pid); |
408 | + } |
409 | + }); |
410 | + } |
411 | + |
412 | + /** |
413 | + * Update weather data from the web |
414 | + */ |
415 | + public override bool update() |
416 | + { |
417 | + debug("Updating Weather"); |
418 | + |
419 | + reload.hide(); |
420 | + |
421 | + label.label = "<span face='Open Sans Light' font='16'>%s</span>".printf(_("Loading weather,\nplease wait.")); |
422 | + label.show(); |
423 | + |
424 | + // Get config values |
425 | + string geo_id = settings.get_int ("weather-id").to_string(); |
426 | unit = settings.get_string ("weather-unit") == "Fahrenheit" ? "f" : "c"; |
427 | - var url = "http://weather.yahooapis.com/forecastrss"; |
428 | - url += "?u=" + unit; |
429 | - url += "&w=" + id; |
430 | - message = new Soup.Message ("GET", url); |
431 | - session.queue_message(message, (session, m) => { |
432 | - var data = (string)message.response_body.data; |
433 | - if (data == null) { |
434 | - label.hide (); |
435 | - reload.show (); |
436 | - return; |
437 | - } |
438 | - |
439 | - var current = get_attributes (data, "yweather:condition", {"temp", "text", "code"}); |
440 | - var forecast = get_attributes (data, "yweather:forecast", {"day", "date", "low", "high", "text", "code"}, |
441 | - data.index_of ("<yweather:forecast")); |
442 | - var forecast2 = get_attributes (data, "yweather:forecast", {"day", "date", "low", "high", "text", "code"}, |
443 | - data.index_of ("<yweather:forecast") + 10); |
444 | - |
445 | - var today = new DateTime.now_local ().format (_("%A")); |
446 | - |
447 | - text = |
448 | - "<span face='Open Sans Light' font='24'>" + today + "</span>" + |
449 | - "<span face='Open Sans Light' font='16'> // <i>" + condition_texts[int.parse (current[2])] +"</i></span>\n" + |
450 | - "<span face='gazetteweather' font='68'>" + condition_codes[int.parse (current[2])] + "</span>" + |
451 | - "<span face='Raleway' weight='100' font='72'> " + current[0] + "</span>" + |
452 | - "<span face='Raleway' weight='100' font='40'> ° " + unit.up () + "</span>\n" + |
453 | - |
454 | - "<span face='gazetteweather' font='30'>" + condition_codes[int.parse (forecast[5])] + "</span>" + |
455 | - "<span face='Open Sans Light' font='26'> " + _(forecast[0]) + " </span>" + |
456 | - "<span face='gazetteweather' font='30'>" + condition_codes[int.parse (forecast2[5])] + "</span>" + |
457 | - "<span face='Open Sans Light' font='26'> " + _(forecast2[0]) + "</span>\n"+ |
458 | - |
459 | - "<span face='Raleway' font='21'>" + forecast[2] + " - " + forecast[3] +"°"+unit.up()+" </span>"+ |
460 | - "<span face='Raleway' font='21'>" + forecast2[2] + " - " + forecast2[3] +"°"+unit.up()+" </span>"; |
461 | - label.label = text; |
462 | - }); |
463 | + |
464 | + update_weather(unit, geo_id); |
465 | + |
466 | return true; |
467 | } |
468 | } |
469 | - |
470 | - |
471 | |
472 | === added file 'src/Services/WeatherData.vala' |
473 | --- src/Services/WeatherData.vala 1970-01-01 00:00:00 +0000 |
474 | +++ src/Services/WeatherData.vala 2017-01-14 20:16:28 +0000 |
475 | @@ -0,0 +1,241 @@ |
476 | +namespace WeatherData |
477 | +{ |
478 | + public const int CONDITION_DEFAULT = 48; |
479 | + |
480 | + public const string [] icons = { |
481 | + "c", // tornado |
482 | + "0", // tropical storm |
483 | + "c", // hurricane |
484 | + "1", // severe thunderstorms |
485 | + "1", // thunderstorms |
486 | + "2", // mixed rain and snow |
487 | + "3", // mixed rain and sleet |
488 | + "2", // mixed snow and sleet |
489 | + "e", // freezing drizzle |
490 | + "3", // drizzle |
491 | + "7", // freezing rain |
492 | + "3", // showers |
493 | + "3", // showers |
494 | + "6", // snow flurries |
495 | + "G", // light snow showers G |
496 | + "#", // blowing snow # |
497 | + "h", // snow " |
498 | + "7", // hail |
499 | + "7", // sleet |
500 | + "f", // dust |
501 | + "f", // foggy |
502 | + "f", // haze |
503 | + "M", // smoky M |
504 | + "c", // blustery |
505 | + "S", // windy S |
506 | + "h", // cold |
507 | + "N", // cloudy N |
508 | + "4", // mostly cloudy (night) 4 |
509 | + "3", // mostly cloudy (day) 3 |
510 | + "K", // partly cloudy (night) K |
511 | + "J", // partly cloudy (day) J |
512 | + "C", // clear (night) C |
513 | + "B", // sunny B |
514 | + "2", // fair (night) 2 |
515 | + "B", // fair (day) B |
516 | + "X", // mixed rain and hail X |
517 | + "1", // hot 1 |
518 | + "1", // isolated thunderstorms |
519 | + "1", // scattered thunderstorms |
520 | + "1", // scattered thunderstorms |
521 | + "e", // scattered showers |
522 | + "#", // heavy snow # |
523 | + "2", // scattered snow showers |
524 | + "#", // heavy snow # |
525 | + "H", // partly cloudy H |
526 | + "1", // thundershowers |
527 | + "2", // snow showers |
528 | + "1", // isolated thundershowers |
529 | + ")" // not available ) |
530 | + }; |
531 | + |
532 | + // Array index is a condition code |
533 | + public const string [] conditions = { |
534 | + "Tornado", |
535 | + "Tropical storm", |
536 | + "Hurricane", |
537 | + "Severe thunderstorms", |
538 | + "Thunderstorms", |
539 | + "Mixed rain and snow", |
540 | + "Mixed rain and sleet", |
541 | + "Mixed snow and sleet", |
542 | + "Freezing drizzle", |
543 | + "Drizzle", |
544 | + "Freezing rain", |
545 | + "Showers", |
546 | + "Showers", |
547 | + "Snow flurries", |
548 | + "Light snow showers", |
549 | + "Blowing snow", |
550 | + "Snow", |
551 | + "Hail", |
552 | + "Sleet", |
553 | + "Dust", |
554 | + "Foggy", |
555 | + "Haze", |
556 | + "Smoky", |
557 | + "Blustery", |
558 | + "Windy", |
559 | + "Cold", |
560 | + "Cloudy", |
561 | + "Mostly cloudy", |
562 | + "Mostly cloudy", |
563 | + "Partly cloudy", |
564 | + "Partly cloudy", |
565 | + "Clear", |
566 | + "Sunny", |
567 | + "Fair", |
568 | + "Fair", |
569 | + "Mixed rain and hail", |
570 | + "Hot", |
571 | + "Isolated thunderstorms", |
572 | + "Scattered thunderstorms", |
573 | + "Scattered thunderstorms", |
574 | + "Scattered showers", |
575 | + "Heavy snow", |
576 | + "Scattered snow showers", |
577 | + "Heavy snow", |
578 | + "Partly cloudy", |
579 | + "Thundershowers", |
580 | + "Snow showers", |
581 | + "Isolated thundershowers", |
582 | + "Not available" |
583 | + }; |
584 | + |
585 | + // Days for translation |
586 | + private const string[] days = { |
587 | + "Mon", |
588 | + "Tue", |
589 | + "Wed", |
590 | + "Thu", |
591 | + "Fri", |
592 | + "Sat", |
593 | + "Sun" |
594 | + }; |
595 | + |
596 | + public struct Forecast |
597 | + { |
598 | + // Condition code and condition name (translated) |
599 | + private int _code; |
600 | + public int code { |
601 | + get { return _code; } |
602 | + set { _code = (value < 0 || value > CONDITION_DEFAULT) ? CONDITION_DEFAULT : value; } |
603 | + } |
604 | + |
605 | + public string condition { |
606 | + get { return _(conditions[code]); } |
607 | + } |
608 | + |
609 | + // Day and day name (translated) |
610 | + private int _day; |
611 | + public int day { |
612 | + get { return _day; } |
613 | + set { _day = (value < 1 || value > 7) ? 1 : value; } |
614 | + } |
615 | + public string day_string { |
616 | + get { return _(days[_day - 1]); } |
617 | + } |
618 | + |
619 | + // Temperature |
620 | + public int low; |
621 | + public int high; |
622 | + |
623 | + public string icon { |
624 | + get { return icons[code]; } |
625 | + } |
626 | + } |
627 | + |
628 | + public struct Weather |
629 | + { |
630 | + private int _code; |
631 | + public int code { |
632 | + get { return _code; } |
633 | + set { _code = (value < 0 || value > CONDITION_DEFAULT) ? CONDITION_DEFAULT : value; } |
634 | + } |
635 | + |
636 | + public string condition { |
637 | + get { return _(conditions[code]); } |
638 | + } |
639 | + |
640 | + public int temp; |
641 | + public GenericArray<Forecast?> forecasts; |
642 | + |
643 | + public string icon { |
644 | + get { return icons[code]; } |
645 | + } |
646 | + } |
647 | + |
648 | + public errordomain ParsingError { |
649 | + READ_ERROR, |
650 | + BAD_CONDITION, |
651 | + BAD_FORECAST |
652 | + } |
653 | + |
654 | + /** |
655 | + * Serialize Weather to string |
656 | + */ |
657 | + public string serialize(ref Weather data) |
658 | + { |
659 | + var builder = new StringBuilder(); |
660 | + builder.append("%d %d\n".printf(data.code, data.temp)); |
661 | + |
662 | + data.forecasts.foreach((f) => { |
663 | + builder.append("%d %d %d %d\n".printf(f.code, f.day, f.low, f.high)); |
664 | + }); |
665 | + |
666 | + return builder.str; |
667 | + } |
668 | + |
669 | + /** |
670 | + * Unserialize Weather from string |
671 | + */ |
672 | + public Weather parse(IOChannel source) throws ParsingError, ConvertError, IOChannelError |
673 | + { |
674 | + string buf; |
675 | + size_t len; |
676 | + size_t term_pos; |
677 | + |
678 | + // Parse condition data |
679 | + if (source.read_line(out buf, out len, out term_pos) != IOStatus.NORMAL) { |
680 | + throw new ParsingError.READ_ERROR("Unable to read condition data"); |
681 | + } |
682 | + |
683 | + string[] values = buf.split(" "); |
684 | + |
685 | + if (values.length < 2) { |
686 | + throw new ParsingError.BAD_CONDITION("Bad condition format"); |
687 | + } |
688 | + |
689 | + var data = Weather(); |
690 | + |
691 | + data.code = int.parse(values[0]); |
692 | + data.temp = int.parse(values[1]); |
693 | + |
694 | + // Parse forecasts |
695 | + data.forecasts = new GenericArray<Forecast?>(); |
696 | + |
697 | + while (source.read_line(out buf, out len, out term_pos) != IOStatus.EOF) |
698 | + { |
699 | + values = buf.split(" "); |
700 | + |
701 | + if (values.length < 4) { |
702 | + throw new ParsingError.BAD_FORECAST("Bad forecast format"); |
703 | + } |
704 | + |
705 | + var f = Forecast(); |
706 | + f.code = int.parse(values[0]); |
707 | + f.day = int.parse(values[1]); |
708 | + f.low = int.parse(values[2]); |
709 | + f.high = int.parse(values[3]); |
710 | + |
711 | + data.forecasts.add(f); |
712 | + } |
713 | + |
714 | + return data; |
715 | + } |
716 | +} |
717 | |
718 | === added directory 'src/Utils' |
719 | === added file 'src/Utils/CMakeLists.txt' |
720 | --- src/Utils/CMakeLists.txt 1970-01-01 00:00:00 +0000 |
721 | +++ src/Utils/CMakeLists.txt 2017-01-14 20:16:28 +0000 |
722 | @@ -0,0 +1,63 @@ |
723 | +#Here we're including the Vala package from ../cmake |
724 | + |
725 | +find_package(Vala REQUIRED) |
726 | + |
727 | +#Now we're including the version module to ensure we have a compatible version |
728 | + |
729 | +include(ValaVersion) |
730 | +ensure_vala_version("0.16.0" MINIMUM) |
731 | + |
732 | +#Now we're including the precompile modules to set things up. |
733 | + |
734 | +include(ValaPrecompile) |
735 | + |
736 | + |
737 | +#We're going to load the PkgConfig module from ../cmake |
738 | +#We do this to ensure we can include required modules. |
739 | +#PkgConfig handles all of the querying of packages for us. |
740 | + |
741 | +#It finds their directories, versions, and if they're installed. |
742 | +find_package(PkgConfig) |
743 | + |
744 | +#Now we're declaring LibSoup and LibXML as our REQUIRE dependancies. |
745 | +#If PkgConfig can't find these, you need to install them in Step 1. |
746 | + |
747 | +pkg_check_modules(DEPS REQUIRED libsoup-2.4 libxml-2.0) |
748 | + |
749 | +#Now we're going to ready the libraries and get their directories to include them. |
750 | + |
751 | +set(CFLAGS |
752 | + ${DEPS_CFLAGS} ${DEPS_CFLAGS_OTHER} |
753 | +) |
754 | +set(LIB_PATHS |
755 | + ${DEPS_LIBRARY_DIRS} |
756 | +) |
757 | +link_directories(${LIB_PATHS}) |
758 | + |
759 | + |
760 | +#Here is where vala precompiles all the *.vala files into *.c files. |
761 | +#Then we compule the *.c files to turn them into a true executable. |
762 | + |
763 | +add_definitions(${CFLAGS}) |
764 | +vala_precompile(VALA_C |
765 | + ${CMAKE_SOURCE_DIR}/src/Services/WeatherData.vala |
766 | + Yahoo.vala |
767 | +PACKAGES |
768 | + libsoup-2.4 |
769 | + libxml-2.0 |
770 | +OPTIONS |
771 | + --vapidir=${CMAKE_CURRENT_SOURCE_DIR}/vapi/ |
772 | + ${VALAFLAGS} |
773 | +) |
774 | + |
775 | +#Here we define our executable name. |
776 | + |
777 | +add_executable(weather-yahoo ${VALA_C}) |
778 | + |
779 | +#We need to link the libraries with our Executable. |
780 | + |
781 | +target_link_libraries(weather-yahoo ${DEPS_LIBRARIES}) |
782 | + |
783 | +#Install Gazette Plug for Switchboard integration |
784 | +install (TARGETS weather-yahoo DESTINATION share/gazette/plugins) |
785 | +#install (TARGETS Gazette DESTINATION lib/plugs/gazette) |
786 | |
787 | === added file 'src/Utils/Yahoo.vala' |
788 | --- src/Utils/Yahoo.vala 1970-01-01 00:00:00 +0000 |
789 | +++ src/Utils/Yahoo.vala 2017-01-14 20:16:28 +0000 |
790 | @@ -0,0 +1,197 @@ |
791 | +using Xml; |
792 | +using WeatherData; |
793 | + |
794 | +// Exit codes |
795 | +const int EXIT_SUCCESS = 0; |
796 | +const int EXIT_FAILURE = 1; |
797 | + |
798 | +// XPath queries |
799 | +const string XPATH_CONDITION = "//*[local-name()='condition']"; |
800 | +const string XPATH_FORECAST = "//*[local-name()='forecast'][%d]"; |
801 | + |
802 | +// HTTP query params |
803 | +const string URL = "https://query.yahooapis.com/v1/public/yql?format=xml&q=%s"; |
804 | +const string QUERY = "select * from weather.forecast where woeid = %d and u = '%s'"; |
805 | + |
806 | +// Days of week (to be able to convert them in integer) |
807 | +const string[] days = { |
808 | + "Mon", |
809 | + "Tue", |
810 | + "Wed", |
811 | + "Thu", |
812 | + "Fri", |
813 | + "Sat", |
814 | + "Sun" |
815 | +}; |
816 | + |
817 | +errordomain ParsingError { |
818 | + CONDITION, |
819 | + CONDITION_FIELDS, |
820 | + FORECAST, |
821 | + FORECAST_FIELDS |
822 | +} |
823 | + |
824 | +/** |
825 | + * Extract XPath node |
826 | + */ |
827 | +Xml.Node* xpath_get_node(XPath.Context ctx, string query) |
828 | +{ |
829 | + Xml.Node* node = null; |
830 | + var obj = ctx.eval_expression(query); |
831 | + |
832 | + if (obj != null) { |
833 | + if (obj->nodesetval != null && !obj->nodesetval->is_empty()) { |
834 | + node = obj->nodesetval->item(0); |
835 | + }; |
836 | + |
837 | + delete obj; |
838 | + } |
839 | + |
840 | + return node; |
841 | +} |
842 | + |
843 | +/** |
844 | + * Parse day of week |
845 | + */ |
846 | +int parse_day(string val) |
847 | +{ |
848 | + for (var i = 0; i < days.length; i++) { |
849 | + if (days[i] == val) { |
850 | + return i + 1; |
851 | + } |
852 | + } |
853 | + |
854 | + return 0; |
855 | +} |
856 | + |
857 | +/** |
858 | + * Parse current condition data from the libxml2 document |
859 | + */ |
860 | +void parse_condition(XPath.Context ctx, ref Weather weather) throws ParsingError |
861 | +{ |
862 | + var node = xpath_get_node(ctx, XPATH_CONDITION); |
863 | + |
864 | + if (node == null) { |
865 | + throw new ParsingError.CONDITION("Unable to find condition data"); |
866 | + } |
867 | + |
868 | + var temp = node->get_prop("temp"); |
869 | + var code = node->get_prop("code"); |
870 | + |
871 | + if (temp == null || code == null) { |
872 | + throw new ParsingError.CONDITION_FIELDS("Some fields of the condition data are empty"); |
873 | + } |
874 | + |
875 | + weather.temp = int.parse(temp); |
876 | + weather.code = int.parse(code); |
877 | +} |
878 | + |
879 | +/** |
880 | + * Parse forecast data from the libxml2 document |
881 | + */ |
882 | +void parse_forecast(XPath.Context ctx, ref Weather weather, int index) throws ParsingError |
883 | +{ |
884 | + var node = xpath_get_node(ctx, XPATH_FORECAST.printf(index)); |
885 | + |
886 | + if (node == null) { |
887 | + throw new ParsingError.FORECAST("Unable to find forecast data"); |
888 | + } |
889 | + |
890 | + var code = node->get_prop("code"); |
891 | + var day = node->get_prop("day"); |
892 | + var low = node->get_prop("low"); |
893 | + var high = node->get_prop("high"); |
894 | + |
895 | + if (code == null || day == null || low == null || high == null) { |
896 | + throw new ParsingError.FORECAST_FIELDS("Some fields of the forecast data are empty"); |
897 | + } |
898 | + |
899 | + var f = Forecast(); |
900 | + f.code = int.parse(code); |
901 | + f.day = parse_day(day); |
902 | + f.low = int.parse(low); |
903 | + f.high = int.parse(high); |
904 | + |
905 | + weather.forecasts.add(f); |
906 | +} |
907 | + |
908 | +/** |
909 | + * Main parsing function |
910 | + */ |
911 | +int parse_response(Soup.Message msg) |
912 | +{ |
913 | + var body = (string) msg.response_body.data; |
914 | + |
915 | + // Fallback if the HTTP response goes wrong |
916 | + if ((msg.status_code != 200) || (body == null)) { |
917 | + stderr.puts("Invalid Yahoo Weather HTTP response\n"); |
918 | + return EXIT_FAILURE; |
919 | + } |
920 | + |
921 | + // Try to load Xml document into internal libxml2 structure |
922 | + var doc = Parser.parse_doc(body); |
923 | + if (doc == null) { |
924 | + stderr.puts("Unable to parse Yahoo Weather XML response\n"); |
925 | + return EXIT_FAILURE; |
926 | + } |
927 | + |
928 | + var ctx = new XPath.Context(doc); |
929 | + if (ctx == null) { |
930 | + stderr.puts("Failed to create XPath context\n"); |
931 | + delete doc; |
932 | + return EXIT_FAILURE; |
933 | + } |
934 | + |
935 | + var weather = Weather(); |
936 | + weather.forecasts = new GenericArray<Forecast?>(); |
937 | + |
938 | + // Parse document values |
939 | + try { |
940 | + parse_condition(ctx, ref weather); |
941 | + |
942 | + parse_forecast(ctx, ref weather, 1); |
943 | + parse_forecast(ctx, ref weather, 2); |
944 | + } |
945 | + catch (ParsingError e) { |
946 | + stderr.puts(e.message + "\n"); |
947 | + return EXIT_FAILURE; |
948 | + } |
949 | + finally { |
950 | + delete doc; |
951 | + } |
952 | + |
953 | + stdout.puts(WeatherData.serialize(ref weather)); |
954 | + return EXIT_SUCCESS; |
955 | +} |
956 | + |
957 | +/** |
958 | + * Entry point |
959 | + */ |
960 | +int main(string[] args) |
961 | +{ |
962 | + if (args.length < 3) { |
963 | + stderr.puts("Not enough actual parameters\n"); |
964 | + return EXIT_FAILURE; |
965 | + } |
966 | + |
967 | + // Handle arguments |
968 | + var unit = args[1]; |
969 | + var woeid = int.parse(args[2]); |
970 | + |
971 | + if (unit != "c" && unit != "f") { |
972 | + stderr.puts("Invalid unit specified\n"); |
973 | + return EXIT_FAILURE; |
974 | + } |
975 | + |
976 | + // Make HTTP request |
977 | + var url = URL.printf( |
978 | + Soup.URI.encode(QUERY.printf(woeid, unit), null) |
979 | + ); |
980 | + |
981 | + var session = new Soup.SessionSync(); |
982 | + var msg = new Soup.Message("GET", url); |
983 | + |
984 | + session.send_message(msg); |
985 | + |
986 | + return parse_response(msg); |
987 | +} |
988 | |
989 | === added file 'src/Utils/YahooTest' |
990 | --- src/Utils/YahooTest 1970-01-01 00:00:00 +0000 |
991 | +++ src/Utils/YahooTest 2017-01-14 20:16:28 +0000 |
992 | @@ -0,0 +1,14 @@ |
993 | +#!/usr/bin/python |
994 | + |
995 | +import sys |
996 | + |
997 | + |
998 | +def main(): |
999 | + #sys.stdout.write("24 -12\n24 Tue -9 -20\n23 Fri -5 -3\n") |
1000 | + #sys.stdout.write("23 -24\n24 Tue -24 -32\n12 Wed -43 -23") |
1001 | + sys.stdout.write("230 -24\n24 5 -24 -32\n125 7 -43 -23") |
1002 | + |
1003 | + return 0 |
1004 | + |
1005 | +if __name__ == "__main__": |
1006 | + sys.exit(main()) |