/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */ /* weather-iwin.c - US National Weather Service IWIN forecast source * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see * . */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include "gweather-private.h" static gboolean hasAttr (xmlNode *node, const char *attr_name, const char *attr_value) { xmlChar *attr; gboolean res = FALSE; if (!node) return res; attr = xmlGetProp (node, (const xmlChar *) attr_name); if (!attr) return res; res = g_str_equal ((const char *)attr, attr_value); xmlFree (attr); return res; } static GSList * parseForecastXml (const char *buff, GWeatherInfo *original_info) { GSList *res = NULL; xmlDocPtr doc; xmlNode *root, *node; g_return_val_if_fail (original_info != NULL, NULL); if (!buff || !*buff) return NULL; #define XC (const xmlChar *) #define isElem(_node,_name) g_str_equal ((const char *)_node->name, _name) doc = xmlParseMemory (buff, strlen (buff)); if (!doc) return NULL; /* Description at http://www.weather.gov/mdl/XML/Design/MDL_XML_Design.pdf */ root = xmlDocGetRootElement (doc); for (node = root->xmlChildrenNode; node; node = node->next) { if (node->name == NULL || node->type != XML_ELEMENT_NODE) continue; if (isElem (node, "data")) { xmlNode *n; char *time_layout = NULL; time_t update_times[7] = {0}; for (n = node->children; n; n = n->next) { if (!n->name) continue; if (isElem (n, "time-layout")) { if (!time_layout && hasAttr (n, "summarization", "24hourly")) { xmlNode *c; int count = 0; for (c = n->children; c && (count < 7 || !time_layout); c = c->next) { if (c->name && !time_layout && isElem (c, "layout-key")) { xmlChar *val = xmlNodeGetContent (c); if (val) { time_layout = g_strdup ((const char *)val); xmlFree (val); } } else if (c->name && isElem (c, "start-valid-time")) { xmlChar *val = xmlNodeGetContent (c); if (val) { GTimeVal tv; if (g_time_val_from_iso8601 ((const char *)val, &tv)) { update_times[count] = tv.tv_sec; } else { update_times[count] = 0; } count++; xmlFree (val); } } } if (count != 7) { /* There can be more than one time-layout element, the other with only few children, which is not the one to use. */ g_free (time_layout); time_layout = NULL; } } } else if (isElem (n, "parameters")) { xmlNode *p; /* time-layout should be always before parameters */ if (!time_layout) break; if (!res) { int i; for (i = 0; i < 7; i++) { GWeatherInfo *nfo = _gweather_info_new_clone (original_info); nfo->current_time = nfo->update = update_times[i]; if (nfo) res = g_slist_append (res, nfo); } } for (p = n->children; p; p = p->next) { if (p->name && isElem (p, "temperature") && hasAttr (p, "time-layout", time_layout)) { xmlNode *c; GSList *at = res; gboolean is_max = hasAttr (p, "type", "maximum"); if (!is_max && !hasAttr (p, "type", "minimum")) break; for (c = p->children; c && at; c = c->next) { if (isElem (c, "value")) { GWeatherInfo *nfo = (GWeatherInfo *)at->data; xmlChar *val = xmlNodeGetContent (c); /* can pass some values as */ if (!val || !*val) { if (is_max) nfo->temp_max = nfo->temp_min; else nfo->temp_min = nfo->temp_max; } else { if (is_max) nfo->temp_max = atof ((const char *)val); else nfo->temp_min = atof ((const char *)val); } if (val) xmlFree (val); nfo->tempMinMaxValid = nfo->tempMinMaxValid || (nfo->temp_max > -999.0 && nfo->temp_min > -999.0); nfo->valid = nfo->tempMinMaxValid; at = at->next; } } } else if (p->name && isElem (p, "weather") && hasAttr (p, "time-layout", time_layout)) { xmlNode *c; GSList *at = res; for (c = p->children; c && at; c = c->next) { if (c->name && isElem (c, "weather-conditions")) { GWeatherInfo *nfo = at->data; xmlChar *val = xmlGetProp (c, XC "weather-summary"); if (val && nfo) { /* Checking from top to bottom, if 'value' contains 'name', then that win, thus put longer (more precise) values to the top. */ unsigned int i; struct _ph_list { const char *name; GWeatherConditionPhenomenon ph; } ph_list[] = { { "Ice Crystals", GWEATHER_PHENOMENON_ICE_CRYSTALS } , { "Volcanic Ash", GWEATHER_PHENOMENON_VOLCANIC_ASH } , { "Blowing Sand", GWEATHER_PHENOMENON_SANDSTORM } , { "Blowing Dust", GWEATHER_PHENOMENON_DUSTSTORM } , { "Blowing Snow", GWEATHER_PHENOMENON_FUNNEL_CLOUD } , { "Drizzle", GWEATHER_PHENOMENON_DRIZZLE } , { "Rain", GWEATHER_PHENOMENON_RAIN } , { "Snow", GWEATHER_PHENOMENON_SNOW } , { "Fog", GWEATHER_PHENOMENON_FOG } , { "Smoke", GWEATHER_PHENOMENON_SMOKE } , { "Sand", GWEATHER_PHENOMENON_SAND } , { "Haze", GWEATHER_PHENOMENON_HAZE } , { "Dust", GWEATHER_PHENOMENON_DUST } /*, { "", GWEATHER_PHENOMENON_SNOW_GRAINS } , { "", GWEATHER_PHENOMENON_ICE_PELLETS } , { "", GWEATHER_PHENOMENON_HAIL } , { "", GWEATHER_PHENOMENON_SMALL_HAIL } , { "", GWEATHER_PHENOMENON_UNKNOWN_PRECIPITATION } , { "", GWEATHER_PHENOMENON_MIST } , { "", GWEATHER_PHENOMENON_SPRAY } , { "", GWEATHER_PHENOMENON_SQUALL } , { "", GWEATHER_PHENOMENON_TORNADO } , { "", GWEATHER_PHENOMENON_DUST_WHIRLS } */ }; struct _sky_list { const char *name; GWeatherSky sky; } sky_list[] = { { "Mostly Sunny", GWEATHER_SKY_BROKEN } , { "Mostly Clear", GWEATHER_SKY_BROKEN } , { "Partly Cloudy", GWEATHER_SKY_SCATTERED } , { "Mostly Cloudy", GWEATHER_SKY_FEW } , { "Sunny", GWEATHER_SKY_CLEAR } , { "Clear", GWEATHER_SKY_CLEAR } , { "Cloudy", GWEATHER_SKY_OVERCAST } , { "Clouds", GWEATHER_SKY_SCATTERED } , { "Rain", GWEATHER_SKY_SCATTERED } , { "Snow", GWEATHER_SKY_SCATTERED } }; nfo->valid = TRUE; for (i = 0; i < G_N_ELEMENTS (ph_list); i++) { if (strstr ((const char *)val, ph_list [i].name)) { nfo->cond.significant = TRUE; nfo->cond.phenomenon = ph_list [i].ph; break; } } for (i = 0; i < G_N_ELEMENTS (sky_list); i++) { if (strstr ((const char *)val, sky_list [i].name)) { nfo->sky = sky_list [i].sky; break; } } } if (val) xmlFree (val); at = at->next; } } } } if (res) { gboolean have_any = FALSE; GSList *r; /* Remove invalid forecast data from the list. They should be all valid or all invalid. */ for (r = res; r; r = r->next) { GWeatherInfo *nfo = r->data; if (!nfo || !nfo->valid) { if (r->data) g_object_unref (r->data); r->data = NULL; } else { have_any = TRUE; if (nfo->tempMinMaxValid) nfo->temp = (nfo->temp_min + nfo->temp_max) / 2.0; } } if (!have_any) { /* data members are freed already */ g_slist_free (res); res = NULL; } } break; } } g_free (time_layout); /* stop seeking XML */ break; } } xmlFreeDoc (doc); #undef XC #undef isElem return res; } static void iwin_finish (SoupSession *session, SoupMessage *msg, gpointer data) { GWeatherInfo *info; WeatherLocation *loc; if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) { /* forecast data is not really interesting anyway ;) */ if (msg->status_code == SOUP_STATUS_CANCELLED) { g_debug ("Failed to get IWIN forecast data: %d %s\n", msg->status_code, msg->reason_phrase); return; } g_warning ("Failed to get IWIN forecast data: %d %s\n", msg->status_code, msg->reason_phrase); _gweather_info_request_done (data, msg); return; } info = data; loc = &info->location; g_debug ("iwin data for %s", loc->zone); g_debug ("%s", msg->response_body->data); info->forecast_list = parseForecastXml (msg->response_body->data, info); _gweather_info_request_done (info, msg); } /* Get forecast into newly alloc'ed string */ gboolean iwin_start_open (GWeatherInfo *info) { gchar *url; WeatherLocation *loc; SoupMessage *msg; struct tm tm; time_t now; g_autofree char *latstr = NULL; g_autofree char *lonstr = NULL; g_assert (info != NULL); loc = &info->location; /* No zone (or -) means no weather information from national offices. We don't actually use zone, but it's a good indicator of a US location. (@ and : prefixes were used in the past for Australia and UK) */ if (!loc->zone || loc->zone[0] == '-' || loc->zone[0] == '@' || loc->zone[0] == ':') { g_debug ("iwin_start_open, ignoring location %s because zone '%s' has no weather info", loc->name, loc->zone ? loc->zone : "(empty)"); return FALSE; } if (!loc->latlon_valid) return FALSE; /* see the description here: http://www.weather.gov/forecasts/xml/ */ now = time (NULL); localtime_r (&now, &tm); latstr = _radians_to_degrees_str (loc->latitude); lonstr = _radians_to_degrees_str (loc->longitude); url = g_strdup_printf ("https://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?&lat=%s&lon=%s&format=24+hourly&startDate=%04d-%02d-%02d&numDays=7", latstr, lonstr, 1900 + tm.tm_year, 1 + tm.tm_mon, tm.tm_mday); g_debug ("iwin_start_open, requesting: %s", url); msg = soup_message_new ("GET", url); _gweather_info_begin_request (info, msg); soup_session_queue_message (info->session, msg, iwin_finish, info); g_free (url); return TRUE; }