Merge lp:~jacob1237/gazette/weatherapi into lp:gazette

Proposed by Jacob S.
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
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.

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://weather.yahooapis.com/forecastrss) with OAuth.

So my fix is using https://query.yahooapis.com/v1/public/yql as a main endpoint.

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.spawn_async_with_pipes and reads stdout and stderr of the child 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.

To post a comment you must log in.

Unmerged revisions

97. By Jacob S.

Weather API completely reworked: now it uses external processes to fetch weather.
Yahoo weather API fix.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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())

Subscribers

People subscribed via source and target branches