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
=== modified file 'CMakeLists.txt'
--- CMakeLists.txt 2013-04-20 16:58:56 +0000
+++ CMakeLists.txt 2017-01-14 20:16:28 +0000
@@ -14,9 +14,10 @@
1414
15set (DATADIR "${CMAKE_INSTALL_PREFIX}/share")15set (DATADIR "${CMAKE_INSTALL_PREFIX}/share")
16set (PKGDATADIR "${DATADIR}/${NAME}")16set (PKGDATADIR "${DATADIR}/${NAME}")
17set (PLUGINDIR "${PKGDATADIR}/plugins")
17set (GETTEXT_PACKAGE "${NAME}")18set (GETTEXT_PACKAGE "${NAME}")
18set (RELEASE_NAME "Simple and functional.")19set (RELEASE_NAME "Simple and functional.")
19set (VERSION "0.1")20set (VERSION "0.2")
20set (VERSION_INFO "Release")21set (VERSION_INFO "Release")
21set (CMAKE_C_FLAGS "-ggdb")22set (CMAKE_C_FLAGS "-ggdb")
22set (PREFIX ${CMAKE_INSTALL_PREFIX})23set (PREFIX ${CMAKE_INSTALL_PREFIX})
@@ -28,7 +29,7 @@
28add_definitions(-DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\")29add_definitions(-DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\")
2930
30find_package(PkgConfig)31find_package(PkgConfig)
31pkg_check_modules(DEPS REQUIRED goa-1.0 libgdata libsoup-2.4 pantheon granite clutter-gtk-1.0 zeitgeist-1.0)32pkg_check_modules(DEPS REQUIRED goa-1.0 libgdata libsoup-2.4 libxml-2.0 pantheon granite clutter-gtk-1.0 zeitgeist-1.0)
3233
33link_libraries(${DEPS_LIBRARIES})34link_libraries(${DEPS_LIBRARIES})
34link_directories(${DEPS_LIBRARY_DIRS})35link_directories(${DEPS_LIBRARY_DIRS})
@@ -45,9 +46,10 @@
45 src/Services/Files.vala46 src/Services/Files.vala
46 src/Services/Service.vala47 src/Services/Service.vala
47 src/Services/News.vala48 src/Services/News.vala
49 src/Services/WeatherData.vala
48 src/Services/Weather.vala50 src/Services/Weather.vala
49 src/Services/Calendar.vala51 src/Services/Calendar.vala
50 src/Widgets/GazetteWindow.vala52 src/Widgets/GazetteWindow.vala
51 src/Widgets/ShadowedLabel.vala53 src/Widgets/ShadowedLabel.vala
52 ${CMAKE_BINARY_DIR}/src/Config.vala54 ${CMAKE_BINARY_DIR}/src/Config.vala
53PACKAGES55PACKAGES
@@ -66,6 +68,7 @@
6668
67add_subdirectory (po)69add_subdirectory (po)
68add_subdirectory (src/Plugs)70add_subdirectory (src/Plugs)
71add_subdirectory (src/Utils)
6972
70include(GSettings)73include(GSettings)
71add_schema ("data/org.pantheon.gazette.gschema.xml")74add_schema ("data/org.pantheon.gazette.gschema.xml")
7275
=== removed file 'src/Plugs/Config.vala.cmake'
--- src/Plugs/Config.vala.cmake 2013-04-20 17:18:50 +0000
+++ src/Plugs/Config.vala.cmake 1970-01-01 00:00:00 +0000
@@ -1,27 +0,0 @@
1//
2// Copyright (C) 2011 Tom Beckmann
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16//
17
18namespace Constants {
19 public const string DATADIR = "@DATADIR@";
20 public const string PKGDATADIR = "@PKGDATADIR@";
21 public const string GETTEXT_PACKAGE = "@GETTEXT_PACKAGE@";
22 public const string RELEASE_NAME = "@RELEASE_NAME@";
23 public const string VERSION = "@VERSION@";
24 public const string VERSION_INFO = "@VERSION_INFO@";
25 public const string PLUGINDIR = "@PLUGINDIR@";
26 public const string SCHEMA = "org.pantheon.gazette";
27}
280
=== modified file 'src/Services/Weather.vala'
--- src/Services/Weather.vala 2013-07-29 10:34:55 +0000
+++ src/Services/Weather.vala 2017-01-14 20:16:28 +0000
@@ -1,213 +1,199 @@
11using WeatherData;
2const string [] condition_codes = {
3 "c", // tornado
4 "0", // tropical storm
5 "c", // hurricane
6 "1", // severe thunderstorms
7 "1", // thunderstorms
8 "2", // mixed rain and snow
9 "3", // mixed rain and sleet
10 "2", // mixed snow and sleet
11 "e", // freezing drizzle
12 "3", // drizzle
13 "7", // freezing rain
14 "3", // showers
15 "3", // showers
16 "6", // snow flurries
17 "G", // light snow showers G
18 "#", // blowing snow #
19 "h", // snow "
20 "7", // hail
21 "7", // sleet
22 "f", // dust
23 "f", // foggy
24 "f", // haze
25 "M", // smoky M
26 "c", // blustery
27 "S", // windy S
28 "h", // cold
29 "N", // cloudy N
30 "4", // mostly cloudy (night) 4
31 "3", // mostly cloudy (day) 3
32 "K", // partly cloudy (night) K
33 "J", // partly cloudy (day) J
34 "C", // clear (night) C
35 "B", // sunny B
36 "2", // fair (night) 2
37 "B", // fair (day) B
38 "X", // mixed rain and hail X
39 "1", // hot 1
40 "1", // isolated thunderstorms
41 "1", // scattered thunderstorms
42 "1", // scattered thunderstorms
43 "e", // scattered showers
44 "#", // heavy snow #
45 "2", // scattered snow showers
46 "#", // heavy snow #
47 "H", // partly cloudy H
48 "1", // thundershowers
49 "2", // snow showers
50 "1", // isolated thundershowers
51 ")" // not available )
52};
532
54public class Weather : Service3public class Weather : Service
55{4{
56 string [] condition_texts = {
57 _("Tornado"), // tornado
58 _("Tropical storm"), // tropical storm
59 _("Hurricane"), // hurricane
60 _("Severe thunderstorms"), // severe thunderstorms
61 _("Thunderstorms"), // thunderstorms
62 _("Mixed rain and snow"), // mixed rain and snow
63 _("Mixed rain and sleet"), // mixed rain and sleet
64 _("Mixed snow and sleet"), // mixed snow and sleet
65 _("Freezing drizzle"), // freezing drizzle
66 _("Drizzle"), // drizzle
67 _("Freezing rain"), // freezing rain
68 _("Showers"), // showers
69 _("Showers"), // showers
70 _("Snow flurries"), // snow flurries
71 _("Light snow showers"), // light snow showers G
72 _("Blowing snow"), // blowing snow #
73 _("Snow"), // snow "
74 _("Hail"), // hail
75 _("Sleet"), // sleet
76 _("Dust"), // dust
77 _("Foggy"), // foggy
78 _("Haze"), // haze
79 _("Smoky"), // smoky M
80 _("Blustery"), // blustery
81 _("Windy"), // windy S
82 _("Cold"), // cold
83 _("Cloudy"), // cloudy N
84 _("Mostly cloudy"), // mostly cloudy (night) 4
85 _("Mostly cloudy"), // mostly cloudy (day) 3
86 _("Partly cloudy"), // partly cloudy (night) K
87 _("Partly cloudy"), // partly cloudy (day) J
88 _("Clear"), // clear (night) C
89 _("Sunny"), // sunny B
90 _("Fair"), // fair (night) 2
91 _("Fair"), // fair (day) B
92 _("Mixed rain and hail"), // mixed rain and hail X
93 _("Hot"), // hot 1
94 _("Isolated thunderstorms"), // isolated thunderstorms
95 _("Scattered thunderstorms"), // scattered thunderstorms
96 _("Scattered thunderstorms"), // scattered thunderstorms
97 _("Scattered showers"), // scattered showers
98 _("Heavy snow"), // heavy snow #
99 _("Scattered snow showers"), // scattered snow showers
100 _("Heavy snow"), // heavy snow #
101 _("Partly cloudy"), // partly cloudy H
102 _("Thundershowers"), // thundershowers
103 _("Snow showers"), // snow showers
104 _("Isolated thundershowers"), // isolated thundershowers
105 _("Not available") // not available )
106 };
107
108 string [] day_texts = {
109 _("Mon"),
110 _("Tue"),
111 _("Wed"),
112 _("Thu"),
113 _("Fri"),
114 _("Sat"),
115 _("Sun")
116 };
117
118 string unit;5 string unit;
119 string text;6
120 Soup.SessionAsync session;
121 Soup.Message message;
122 Settings settings;7 Settings settings;
8 WeatherData.Weather weather;
9
123 ShadowedLabel label;10 ShadowedLabel label;
124 ShadowedLabel reload;11 ShadowedLabel reload;
12512
13 protected string cmd;
14 protected const string default_cmd = "weather-yahoo";
15
16 /**
17 * Constructor
18 */
126 public Weather ()19 public Weather ()
127 {20 {
128 base ("weather");21 base ("weather");
22
23 init_external_command();
24
129 settings = new Settings ("org.pantheon.gazette.weather");25 settings = new Settings ("org.pantheon.gazette.weather");
130 settings.changed.connect( (key) => { update(); });26 settings.changed.connect( (key) => { update(); });
27
131 label = new ShadowedLabel("");28 label = new ShadowedLabel("");
132 reload = get_reload_label(_("weather"));29 reload = get_reload_label(_("weather"));
133 reload.hide();30 reload.hide();
134 session = new Soup.SessionAsync ();31
135 add_child (label);32 add_child (label);
136 add_child (reload);33 add_child (reload);
34
137 load();35 load();
138 Timeout.add(settings.get_int("update-interval"), update); 36
139 }37 Timeout.add(settings.get_int("update-interval"), update);
14038 }
141 public override void create ()39
142 {40 public override void create () {}
14341
144 }42 /**
14543 * Initialize external command path
146 public string[] get_attributes (string data, string tagname, string [] attrs, int offset = 0)44 */
147 {45 protected void init_external_command()
148 var start = data.index_of ("<" + tagname, offset) + tagname.length + 1;46 {
149 var end = data.index_of ("/>", start);47 var weather_cmd = Environment.get_variable("CUSTOM_WEATHER_CMD");
150 var tmp_data = data.substring (start, end - start);48
151 49 cmd = (weather_cmd != null)
152 var res = new string[attrs.length];50 ? weather_cmd
153 for (var i = 0; i < attrs.length; i++) {51 : Constants.PLUGINDIR + "/" + default_cmd;
154 res[i] = get_attribute_value (tmp_data, attrs[i]);52
155 }53 // Check command existence
156 return res;54 var f = File.new_for_path(cmd);
157 }55
15856 if (!f.query_exists() || cmd.length == 0) {
159 public string get_attribute_value (string data, string attr)57 error("Unable to find weather utility. Please check your program installation.");
160 {58 }
161 var start = data.index_of (attr + "=\"") + attr.length + 2;59 }
162 var end = data.index_of ("\"", start);60
163 61 /**
164 return data.substring (start, end - start);62 * Show "reload" message
165 }63 */
166 public override bool update() {64 protected void show_reload()
167 debug ("Updating Weather");65 {
168 reload.hide ();66 label.hide();
169 label.label = "<span face='Open Sans Light' font='16'>" + _("Loading weather,\nplease wait.") + "</span>";67 reload.show();
170 label.show ();68 }
171 string id = settings.get_int ("weather-id").to_string();69
70 /**
71 * Draw weather data on label
72 */
73 protected void draw_weather()
74 {
75 if (weather.forecasts.length < 2) {
76 warning("Not enough forecasts for successful render");
77 return;
78 }
79
80 // Variables shortcuts
81 unowned WeatherData.Weather w = weather;
82 unowned WeatherData.Forecast f1 = w.forecasts.get(0);
83 unowned WeatherData.Forecast f2 = w.forecasts.get(1);
84
85 // Current day and unit
86 var today = new DateTime.now_local().format(_("%A"));
87 var unit_name = unit.up();
88
89 label.label =
90 @"<span face='Open Sans Light' font='24'>$today</span>\n" +
91 @"<span face='Open Sans Light' font='16'><i>$(w.condition)</i></span>\n" +
92 @"<span face='gazetteweather' font='68'>$(w.icon)</span>" +
93 @"<span face='Raleway' weight='100' font='72'> $(w.temp)</span>" +
94 @"<span face='Raleway' weight='100' font='40'> ° $unit_name</span>\n" +
95
96 // Forecast 1
97 @"<span face='gazetteweather' font='30'>$(f1.icon)</span>" +
98 @"<span face='Open Sans Light' font='26'> $(f1.day_string) </span>" +
99
100 // Forecast 2
101 @"<span face='gazetteweather' font='30'>$(f2.icon)</span>" +
102 @"<span face='Open Sans Light' font='26'> $(f2.day_string)</span>\n" +
103
104 @"<span face='Raleway' font='21'>$(f1.low) - $(f1.high)°$unit_name </span>" +
105 @"<span face='Raleway' font='21'>$(f2.low) - $(f2.high)°$unit_name </span>";
106 }
107
108 /**
109 * Spawn external process and read weather data
110 */
111 protected void update_weather(string unit, string geo_id)
112 {
113 Pid? child_pid;
114
115 int standard_input;
116 int? standard_output;
117 int? standard_error;
118
119 string[] spawn_env = Environ.get();
120 string[] spawn_args = {cmd, unit, geo_id};
121
122 try {
123 bool result = Process.spawn_async_with_pipes("/",
124 spawn_args,
125 spawn_env,
126 SpawnFlags.DO_NOT_REAP_CHILD,
127 null,
128 out child_pid,
129 out standard_input,
130 out standard_output,
131 out standard_error);
132
133 if (!result || child_pid == null || standard_output == null) {
134 throw new SpawnError.FAILED("Unable to spawn weather utility process");
135 }
136 }
137 catch (SpawnError e) {
138 critical(e.message);
139
140 show_reload();
141 return;
142 }
143
144 // Wait for process termination
145 ChildWatch.add(child_pid, (pid, status) =>
146 {
147 string buf;
148 size_t len;
149
150 try {
151 if (status == 0) {
152 var output = new IOChannel.unix_new(standard_output);
153
154 weather = WeatherData.parse(output);
155 output.shutdown(false);
156
157 draw_weather();
158 }
159 else {
160 var errors = new IOChannel.unix_new(standard_error);
161
162 errors.read_to_end(out buf, out len);
163 warning(buf);
164
165 errors.shutdown(false);
166
167 show_reload();
168 }
169 }
170 catch (Error e) {
171 warning(e.message);
172 }
173 finally {
174 Process.close_pid(pid);
175 }
176 });
177 }
178
179 /**
180 * Update weather data from the web
181 */
182 public override bool update()
183 {
184 debug("Updating Weather");
185
186 reload.hide();
187
188 label.label = "<span face='Open Sans Light' font='16'>%s</span>".printf(_("Loading weather,\nplease wait."));
189 label.show();
190
191 // Get config values
192 string geo_id = settings.get_int ("weather-id").to_string();
172 unit = settings.get_string ("weather-unit") == "Fahrenheit" ? "f" : "c";193 unit = settings.get_string ("weather-unit") == "Fahrenheit" ? "f" : "c";
173 var url = "http://weather.yahooapis.com/forecastrss";194
174 url += "?u=" + unit;195 update_weather(unit, geo_id);
175 url += "&w=" + id;196
176 message = new Soup.Message ("GET", url);
177 session.queue_message(message, (session, m) => {
178 var data = (string)message.response_body.data;
179 if (data == null) {
180 label.hide ();
181 reload.show ();
182 return;
183 }
184
185 var current = get_attributes (data, "yweather:condition", {"temp", "text", "code"});
186 var forecast = get_attributes (data, "yweather:forecast", {"day", "date", "low", "high", "text", "code"},
187 data.index_of ("<yweather:forecast"));
188 var forecast2 = get_attributes (data, "yweather:forecast", {"day", "date", "low", "high", "text", "code"},
189 data.index_of ("<yweather:forecast") + 10);
190
191 var today = new DateTime.now_local ().format (_("%A"));
192
193 text =
194 "<span face='Open Sans Light' font='24'>" + today + "</span>" +
195 "<span face='Open Sans Light' font='16'> // <i>" + condition_texts[int.parse (current[2])] +"</i></span>\n" +
196 "<span face='gazetteweather' font='68'>" + condition_codes[int.parse (current[2])] + "</span>" +
197 "<span face='Raleway' weight='100' font='72'> " + current[0] + "</span>" +
198 "<span face='Raleway' weight='100' font='40'> ° " + unit.up () + "</span>\n" +
199
200 "<span face='gazetteweather' font='30'>" + condition_codes[int.parse (forecast[5])] + "</span>" +
201 "<span face='Open Sans Light' font='26'> " + _(forecast[0]) + " </span>" +
202 "<span face='gazetteweather' font='30'>" + condition_codes[int.parse (forecast2[5])] + "</span>" +
203 "<span face='Open Sans Light' font='26'> " + _(forecast2[0]) + "</span>\n"+
204
205 "<span face='Raleway' font='21'>" + forecast[2] + " - " + forecast[3] +"°"+unit.up()+" </span>"+
206 "<span face='Raleway' font='21'>" + forecast2[2] + " - " + forecast2[3] +"°"+unit.up()+" </span>";
207 label.label = text;
208 });
209 return true;197 return true;
210 }198 }
211}199}
212
213
214200
=== added file 'src/Services/WeatherData.vala'
--- src/Services/WeatherData.vala 1970-01-01 00:00:00 +0000
+++ src/Services/WeatherData.vala 2017-01-14 20:16:28 +0000
@@ -0,0 +1,241 @@
1namespace WeatherData
2{
3 public const int CONDITION_DEFAULT = 48;
4
5 public const string [] icons = {
6 "c", // tornado
7 "0", // tropical storm
8 "c", // hurricane
9 "1", // severe thunderstorms
10 "1", // thunderstorms
11 "2", // mixed rain and snow
12 "3", // mixed rain and sleet
13 "2", // mixed snow and sleet
14 "e", // freezing drizzle
15 "3", // drizzle
16 "7", // freezing rain
17 "3", // showers
18 "3", // showers
19 "6", // snow flurries
20 "G", // light snow showers G
21 "#", // blowing snow #
22 "h", // snow "
23 "7", // hail
24 "7", // sleet
25 "f", // dust
26 "f", // foggy
27 "f", // haze
28 "M", // smoky M
29 "c", // blustery
30 "S", // windy S
31 "h", // cold
32 "N", // cloudy N
33 "4", // mostly cloudy (night) 4
34 "3", // mostly cloudy (day) 3
35 "K", // partly cloudy (night) K
36 "J", // partly cloudy (day) J
37 "C", // clear (night) C
38 "B", // sunny B
39 "2", // fair (night) 2
40 "B", // fair (day) B
41 "X", // mixed rain and hail X
42 "1", // hot 1
43 "1", // isolated thunderstorms
44 "1", // scattered thunderstorms
45 "1", // scattered thunderstorms
46 "e", // scattered showers
47 "#", // heavy snow #
48 "2", // scattered snow showers
49 "#", // heavy snow #
50 "H", // partly cloudy H
51 "1", // thundershowers
52 "2", // snow showers
53 "1", // isolated thundershowers
54 ")" // not available )
55 };
56
57 // Array index is a condition code
58 public const string [] conditions = {
59 "Tornado",
60 "Tropical storm",
61 "Hurricane",
62 "Severe thunderstorms",
63 "Thunderstorms",
64 "Mixed rain and snow",
65 "Mixed rain and sleet",
66 "Mixed snow and sleet",
67 "Freezing drizzle",
68 "Drizzle",
69 "Freezing rain",
70 "Showers",
71 "Showers",
72 "Snow flurries",
73 "Light snow showers",
74 "Blowing snow",
75 "Snow",
76 "Hail",
77 "Sleet",
78 "Dust",
79 "Foggy",
80 "Haze",
81 "Smoky",
82 "Blustery",
83 "Windy",
84 "Cold",
85 "Cloudy",
86 "Mostly cloudy",
87 "Mostly cloudy",
88 "Partly cloudy",
89 "Partly cloudy",
90 "Clear",
91 "Sunny",
92 "Fair",
93 "Fair",
94 "Mixed rain and hail",
95 "Hot",
96 "Isolated thunderstorms",
97 "Scattered thunderstorms",
98 "Scattered thunderstorms",
99 "Scattered showers",
100 "Heavy snow",
101 "Scattered snow showers",
102 "Heavy snow",
103 "Partly cloudy",
104 "Thundershowers",
105 "Snow showers",
106 "Isolated thundershowers",
107 "Not available"
108 };
109
110 // Days for translation
111 private const string[] days = {
112 "Mon",
113 "Tue",
114 "Wed",
115 "Thu",
116 "Fri",
117 "Sat",
118 "Sun"
119 };
120
121 public struct Forecast
122 {
123 // Condition code and condition name (translated)
124 private int _code;
125 public int code {
126 get { return _code; }
127 set { _code = (value < 0 || value > CONDITION_DEFAULT) ? CONDITION_DEFAULT : value; }
128 }
129
130 public string condition {
131 get { return _(conditions[code]); }
132 }
133
134 // Day and day name (translated)
135 private int _day;
136 public int day {
137 get { return _day; }
138 set { _day = (value < 1 || value > 7) ? 1 : value; }
139 }
140 public string day_string {
141 get { return _(days[_day - 1]); }
142 }
143
144 // Temperature
145 public int low;
146 public int high;
147
148 public string icon {
149 get { return icons[code]; }
150 }
151 }
152
153 public struct Weather
154 {
155 private int _code;
156 public int code {
157 get { return _code; }
158 set { _code = (value < 0 || value > CONDITION_DEFAULT) ? CONDITION_DEFAULT : value; }
159 }
160
161 public string condition {
162 get { return _(conditions[code]); }
163 }
164
165 public int temp;
166 public GenericArray<Forecast?> forecasts;
167
168 public string icon {
169 get { return icons[code]; }
170 }
171 }
172
173 public errordomain ParsingError {
174 READ_ERROR,
175 BAD_CONDITION,
176 BAD_FORECAST
177 }
178
179 /**
180 * Serialize Weather to string
181 */
182 public string serialize(ref Weather data)
183 {
184 var builder = new StringBuilder();
185 builder.append("%d %d\n".printf(data.code, data.temp));
186
187 data.forecasts.foreach((f) => {
188 builder.append("%d %d %d %d\n".printf(f.code, f.day, f.low, f.high));
189 });
190
191 return builder.str;
192 }
193
194 /**
195 * Unserialize Weather from string
196 */
197 public Weather parse(IOChannel source) throws ParsingError, ConvertError, IOChannelError
198 {
199 string buf;
200 size_t len;
201 size_t term_pos;
202
203 // Parse condition data
204 if (source.read_line(out buf, out len, out term_pos) != IOStatus.NORMAL) {
205 throw new ParsingError.READ_ERROR("Unable to read condition data");
206 }
207
208 string[] values = buf.split(" ");
209
210 if (values.length < 2) {
211 throw new ParsingError.BAD_CONDITION("Bad condition format");
212 }
213
214 var data = Weather();
215
216 data.code = int.parse(values[0]);
217 data.temp = int.parse(values[1]);
218
219 // Parse forecasts
220 data.forecasts = new GenericArray<Forecast?>();
221
222 while (source.read_line(out buf, out len, out term_pos) != IOStatus.EOF)
223 {
224 values = buf.split(" ");
225
226 if (values.length < 4) {
227 throw new ParsingError.BAD_FORECAST("Bad forecast format");
228 }
229
230 var f = Forecast();
231 f.code = int.parse(values[0]);
232 f.day = int.parse(values[1]);
233 f.low = int.parse(values[2]);
234 f.high = int.parse(values[3]);
235
236 data.forecasts.add(f);
237 }
238
239 return data;
240 }
241}
0242
=== added directory 'src/Utils'
=== added file 'src/Utils/CMakeLists.txt'
--- src/Utils/CMakeLists.txt 1970-01-01 00:00:00 +0000
+++ src/Utils/CMakeLists.txt 2017-01-14 20:16:28 +0000
@@ -0,0 +1,63 @@
1#Here we're including the Vala package from ../cmake
2
3find_package(Vala REQUIRED)
4
5#Now we're including the version module to ensure we have a compatible version
6
7include(ValaVersion)
8ensure_vala_version("0.16.0" MINIMUM)
9
10#Now we're including the precompile modules to set things up.
11
12include(ValaPrecompile)
13
14
15#We're going to load the PkgConfig module from ../cmake
16#We do this to ensure we can include required modules.
17#PkgConfig handles all of the querying of packages for us.
18
19#It finds their directories, versions, and if they're installed.
20find_package(PkgConfig)
21
22#Now we're declaring LibSoup and LibXML as our REQUIRE dependancies.
23#If PkgConfig can't find these, you need to install them in Step 1.
24
25pkg_check_modules(DEPS REQUIRED libsoup-2.4 libxml-2.0)
26
27#Now we're going to ready the libraries and get their directories to include them.
28
29set(CFLAGS
30 ${DEPS_CFLAGS} ${DEPS_CFLAGS_OTHER}
31)
32set(LIB_PATHS
33 ${DEPS_LIBRARY_DIRS}
34)
35link_directories(${LIB_PATHS})
36
37
38#Here is where vala precompiles all the *.vala files into *.c files.
39#Then we compule the *.c files to turn them into a true executable.
40
41add_definitions(${CFLAGS})
42vala_precompile(VALA_C
43 ${CMAKE_SOURCE_DIR}/src/Services/WeatherData.vala
44 Yahoo.vala
45PACKAGES
46 libsoup-2.4
47 libxml-2.0
48OPTIONS
49 --vapidir=${CMAKE_CURRENT_SOURCE_DIR}/vapi/
50 ${VALAFLAGS}
51)
52
53#Here we define our executable name.
54
55add_executable(weather-yahoo ${VALA_C})
56
57#We need to link the libraries with our Executable.
58
59target_link_libraries(weather-yahoo ${DEPS_LIBRARIES})
60
61#Install Gazette Plug for Switchboard integration
62install (TARGETS weather-yahoo DESTINATION share/gazette/plugins)
63#install (TARGETS Gazette DESTINATION lib/plugs/gazette)
064
=== added file 'src/Utils/Yahoo.vala'
--- src/Utils/Yahoo.vala 1970-01-01 00:00:00 +0000
+++ src/Utils/Yahoo.vala 2017-01-14 20:16:28 +0000
@@ -0,0 +1,197 @@
1using Xml;
2using WeatherData;
3
4// Exit codes
5const int EXIT_SUCCESS = 0;
6const int EXIT_FAILURE = 1;
7
8// XPath queries
9const string XPATH_CONDITION = "//*[local-name()='condition']";
10const string XPATH_FORECAST = "//*[local-name()='forecast'][%d]";
11
12// HTTP query params
13const string URL = "https://query.yahooapis.com/v1/public/yql?format=xml&q=%s";
14const string QUERY = "select * from weather.forecast where woeid = %d and u = '%s'";
15
16// Days of week (to be able to convert them in integer)
17const string[] days = {
18 "Mon",
19 "Tue",
20 "Wed",
21 "Thu",
22 "Fri",
23 "Sat",
24 "Sun"
25};
26
27errordomain ParsingError {
28 CONDITION,
29 CONDITION_FIELDS,
30 FORECAST,
31 FORECAST_FIELDS
32}
33
34/**
35 * Extract XPath node
36 */
37Xml.Node* xpath_get_node(XPath.Context ctx, string query)
38{
39 Xml.Node* node = null;
40 var obj = ctx.eval_expression(query);
41
42 if (obj != null) {
43 if (obj->nodesetval != null && !obj->nodesetval->is_empty()) {
44 node = obj->nodesetval->item(0);
45 };
46
47 delete obj;
48 }
49
50 return node;
51}
52
53/**
54 * Parse day of week
55 */
56int parse_day(string val)
57{
58 for (var i = 0; i < days.length; i++) {
59 if (days[i] == val) {
60 return i + 1;
61 }
62 }
63
64 return 0;
65}
66
67/**
68 * Parse current condition data from the libxml2 document
69 */
70void parse_condition(XPath.Context ctx, ref Weather weather) throws ParsingError
71{
72 var node = xpath_get_node(ctx, XPATH_CONDITION);
73
74 if (node == null) {
75 throw new ParsingError.CONDITION("Unable to find condition data");
76 }
77
78 var temp = node->get_prop("temp");
79 var code = node->get_prop("code");
80
81 if (temp == null || code == null) {
82 throw new ParsingError.CONDITION_FIELDS("Some fields of the condition data are empty");
83 }
84
85 weather.temp = int.parse(temp);
86 weather.code = int.parse(code);
87}
88
89/**
90 * Parse forecast data from the libxml2 document
91 */
92void parse_forecast(XPath.Context ctx, ref Weather weather, int index) throws ParsingError
93{
94 var node = xpath_get_node(ctx, XPATH_FORECAST.printf(index));
95
96 if (node == null) {
97 throw new ParsingError.FORECAST("Unable to find forecast data");
98 }
99
100 var code = node->get_prop("code");
101 var day = node->get_prop("day");
102 var low = node->get_prop("low");
103 var high = node->get_prop("high");
104
105 if (code == null || day == null || low == null || high == null) {
106 throw new ParsingError.FORECAST_FIELDS("Some fields of the forecast data are empty");
107 }
108
109 var f = Forecast();
110 f.code = int.parse(code);
111 f.day = parse_day(day);
112 f.low = int.parse(low);
113 f.high = int.parse(high);
114
115 weather.forecasts.add(f);
116}
117
118/**
119 * Main parsing function
120 */
121int parse_response(Soup.Message msg)
122{
123 var body = (string) msg.response_body.data;
124
125 // Fallback if the HTTP response goes wrong
126 if ((msg.status_code != 200) || (body == null)) {
127 stderr.puts("Invalid Yahoo Weather HTTP response\n");
128 return EXIT_FAILURE;
129 }
130
131 // Try to load Xml document into internal libxml2 structure
132 var doc = Parser.parse_doc(body);
133 if (doc == null) {
134 stderr.puts("Unable to parse Yahoo Weather XML response\n");
135 return EXIT_FAILURE;
136 }
137
138 var ctx = new XPath.Context(doc);
139 if (ctx == null) {
140 stderr.puts("Failed to create XPath context\n");
141 delete doc;
142 return EXIT_FAILURE;
143 }
144
145 var weather = Weather();
146 weather.forecasts = new GenericArray<Forecast?>();
147
148 // Parse document values
149 try {
150 parse_condition(ctx, ref weather);
151
152 parse_forecast(ctx, ref weather, 1);
153 parse_forecast(ctx, ref weather, 2);
154 }
155 catch (ParsingError e) {
156 stderr.puts(e.message + "\n");
157 return EXIT_FAILURE;
158 }
159 finally {
160 delete doc;
161 }
162
163 stdout.puts(WeatherData.serialize(ref weather));
164 return EXIT_SUCCESS;
165}
166
167/**
168 * Entry point
169 */
170int main(string[] args)
171{
172 if (args.length < 3) {
173 stderr.puts("Not enough actual parameters\n");
174 return EXIT_FAILURE;
175 }
176
177 // Handle arguments
178 var unit = args[1];
179 var woeid = int.parse(args[2]);
180
181 if (unit != "c" && unit != "f") {
182 stderr.puts("Invalid unit specified\n");
183 return EXIT_FAILURE;
184 }
185
186 // Make HTTP request
187 var url = URL.printf(
188 Soup.URI.encode(QUERY.printf(woeid, unit), null)
189 );
190
191 var session = new Soup.SessionSync();
192 var msg = new Soup.Message("GET", url);
193
194 session.send_message(msg);
195
196 return parse_response(msg);
197}
0198
=== added file 'src/Utils/YahooTest'
--- src/Utils/YahooTest 1970-01-01 00:00:00 +0000
+++ src/Utils/YahooTest 2017-01-14 20:16:28 +0000
@@ -0,0 +1,14 @@
1#!/usr/bin/python
2
3import sys
4
5
6def main():
7 #sys.stdout.write("24 -12\n24 Tue -9 -20\n23 Fri -5 -3\n")
8 #sys.stdout.write("23 -24\n24 Tue -24 -32\n12 Wed -43 -23")
9 sys.stdout.write("230 -24\n24 5 -24 -32\n125 7 -43 -23")
10
11 return 0
12
13if __name__ == "__main__":
14 sys.exit(main())

Subscribers

People subscribed via source and target branches