868737a07fa8af9f539cdc579ec460ce72bca8d3
[monky] / src / weather.c
1 /*
2  * Conky, a system monitor, based on torsmo
3  *
4  * Please see COPYING for details
5  *
6  * Copyright (c) 2005-2009 Brenden Matthews, Philip Kovacs, et. al.
7  *      (see AUTHORS)
8  * All rights reserved.
9  *
10  * This program is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  * You should have received a copy of the GNU General Public License
20  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21  *
22  */
23
24 /*
25  * TODO: Add weather forecast info from weather.com
26  *
27  */
28
29 #include "conky.h"
30 #include "logging.h"
31 #include "weather.h"
32 #include "temphelper.h"
33 #include <time.h>
34 #include <ctype.h>
35 #ifdef MATH
36 #include <math.h>
37 #endif /* MATH */
38 #include <curl/curl.h>
39 #include <curl/types.h>
40 #include <curl/easy.h>
41 #ifdef XOAP
42 #include <libxml/parser.h>
43 #endif /* XOAP */
44
45 /* Possible sky conditions */
46 #define NUM_CC_CODES 6
47 const char *CC_CODES[NUM_CC_CODES] = {
48         "SKC", "CLR", "FEW", "SCT", "BKN", "OVC"
49 };
50
51 /* Possible weather modifiers */
52 #define NUM_WM_CODES 9
53 const char *WM_CODES[NUM_WM_CODES] = {
54         "VC", "MI", "BC", "PR", "TS", "BL",
55         "SH", "DR", "FZ"
56 };
57
58 /* Possible weather conditions */
59 #define NUM_WC_CODES 17
60 const char *WC_CODES[NUM_WC_CODES] = {
61         "DZ", "RA", "GR", "GS", "SN", "SG",
62         "FG", "HZ", "FU", "BR", "DU", "SA",
63         "FC", "PO", "SQ", "SS", "DS"
64 };
65
66 typedef struct location_ {
67         char *uri;
68         int last_update;
69         PWEATHER data;
70         timed_thread *p_timed_thread;
71         struct location_ *next;
72 } location;
73
74 static location *locations_head = 0;
75
76 location *find_location(char *uri)
77 {
78         location *tail = locations_head;
79         location *new = 0;
80         while (tail) {
81                 if (tail->uri &&
82                                 strcmp(tail->uri, uri) == EQUAL) {
83                         return tail;
84                 }
85                 tail = tail->next;
86         }
87         if (!tail) { // new location!!!!!!!
88                 new = malloc(sizeof(location));
89                 memset(new, 0, sizeof(location));
90                 new->uri = strndup(uri, text_buffer_size);
91                 tail = locations_head;
92                 while (tail && tail->next) {
93                         tail = tail->next;
94                 }
95                 if (!tail) {
96                         // omg the first one!!!!!!!
97                         locations_head = new;
98                 } else {
99                         tail->next = new;
100                 }
101         }
102         return new;
103 }
104
105 void free_weather_info(void)
106 {
107         location *tail = locations_head;
108         location *last = 0;
109
110         while (tail) {
111                 if (tail->uri) free(tail->uri);
112                 last = tail;
113                 tail = tail->next;
114                 free(last);
115         }
116         locations_head = 0;
117 }
118
119 int rel_humidity(int dew_point, int air) {
120         const float a = 17.27f;
121         const float b = 237.7f;
122
123         float diff = a*(dew_point/(b+dew_point)-air/(b+air));
124 #ifdef MATH
125         return (int)(100.f*expf(diff));
126 #else
127         return (int)(16.666667163372f*(6.f+diff*(6.f+diff*(3.f+diff))));
128 #endif /* MATH */
129 }
130
131 #ifdef XOAP
132 //TODO: Lets get rid of the recursion
133 static void parse_cc(PWEATHER *res, xmlNodePtr cc)
134 {
135
136         xmlNodePtr cur = NULL;
137
138         for (cur = cc; cur; cur = cur->next) {
139                 if (cur->type == XML_ELEMENT_NODE) {
140                         if (!xmlStrcmp(cur->name, (const xmlChar *) "lsup")) {
141                                 strncpy(res->lastupd, (char *)cur->children->content, 31);
142                         } else if       (!xmlStrcmp(cur->name, (const xmlChar *) "tmp")) {
143                                 res->temp = atoi((char *)cur->children->content);
144                         } else if (!xmlStrcmp(cur->name, (const xmlChar *) "t")) {
145                                 if(res->xoap_t[0] == '\0') {
146                                         strncpy(res->xoap_t, (char *)cur->children->content, 31);
147                                 }
148                         } else if (!xmlStrcmp(cur->name, (const xmlChar *) "r")) {
149                                 res->bar = atoi((char *)cur->children->content);
150                         } else if (!xmlStrcmp(cur->name, (const xmlChar *) "s")) {
151                                 res->wind_s = atoi((char *)cur->children->content);
152                         } else if (!xmlStrcmp(cur->name, (const xmlChar *) "d")) {
153                                 if (isdigit((char)cur->children->content[0])) {
154                                         res->wind_d = atoi((char *)cur->children->content);
155                                 }
156                         } else if (!xmlStrcmp(cur->name, (const xmlChar *) "hmid")) {
157                                 res->hmid = atoi((char *)cur->children->content);
158                         }
159                 }
160                 parse_cc(res, cur->children);
161         }
162         return;
163 }
164
165 static void parse_weather_xml(PWEATHER *res, const char *data)
166 {
167         xmlDocPtr doc;
168         xmlNodePtr cur;
169
170         if (!(doc = xmlReadMemory(data, strlen(data), "", NULL, 0))) {
171                 ERR("weather: can't read xml data");
172                 return;
173         }
174
175         cur = xmlDocGetRootElement(doc);
176
177         while(cur) {
178                 if (cur->type == XML_ELEMENT_NODE) {
179                         if (!xmlStrcmp(cur->name, (const xmlChar *) "weather")) {
180                                 cur = cur->children;
181                                 while (cur != NULL) {
182                                         if (cur->type == XML_ELEMENT_NODE) {
183                                                 if (!xmlStrcmp(cur->name, (const xmlChar *) "cc")) {
184                                                         parse_cc(res, cur->children);
185                                                         xmlFreeDoc(doc);
186                                                         return;
187                                                 }
188                                         }
189                                         cur = cur->next;
190                                 }
191                         }
192                 }
193                 cur = cur->next;
194         }
195
196         ERR("weather: incorrect xml data");
197         xmlFreeDoc(doc);
198         return ;
199 }
200 #endif /* XOAP */
201
202 /*
203  * Horrible hack to avoid using regexes
204  *
205  */
206
207 static inline void parse_token(PWEATHER *res, char *token) {
208
209         int i;
210         char s_tmp[64];
211
212         switch (strlen(token)) {
213
214                 //Check all tokens 2 chars long
215                 case 2:
216
217                         //Check if token is a weather condition
218                         for (i=0; i<2; i++) {
219                                 if (!isalpha(token[i])) break;
220                         }
221                         if (i==2) {
222                                 for(i=0; i<NUM_WC_CODES; i++) {
223                                         if (!strncmp(token, WC_CODES[i], 2)) {
224                                                 res->wc=i+1;
225                                                 break;
226                                         }
227                                 }
228                                 return;
229                         }
230
231                         //Check for CB
232                         if (!strcmp(token, "CB")) {
233                                 res->cc = 8;
234                                 return;
235                         }
236
237                         break;
238
239                         //Check all tokens 3 chars long
240                 case 3:
241
242                         //Check if token is a modified weather condition
243                         if ((token[0] == '+') || (token[0] == '-')) {
244                                 for (i=1; i<3; i++) {
245                                         if (!isalpha(token[i])) break;
246                                 }
247                                 if (i==3) {
248                                         for(i=0; i<NUM_WC_CODES; i++) {
249                                                 if (!strncmp(&token[1], WC_CODES[i], 2)) {
250                                                         res->wc=i+1;
251                                                         break;
252                                                 }
253                                         }
254                                         return;
255                                 }
256                         }
257
258                         //Check for NCD or NSC
259                         if ((!strcmp(token, "NCD")) || (!strcmp(token, "NSC"))) {
260                                 res->cc = 1;
261                                 return;
262                         }
263
264                         //Check for TCU
265                         if (!strcmp(token, "TCU")) {
266                                 res->cc = 7;
267                                 return;
268                         }
269
270                         break;
271
272                         //Check all tokens 4 chars long
273                 case 4:
274
275                         //Check if token is a modified weather condition
276                         for(i=0; i<NUM_WM_CODES; i++) {
277                                 if (!strncmp(token, WM_CODES[i], 2)) {
278                                         for(i=0; i<NUM_WC_CODES; i++) {
279                                                 if (!strncmp(&token[2], WC_CODES[i], 2)) {
280                                                         res->wc=i+1;
281                                                         return;
282                                                 }
283                                         }
284                                         break;
285                                 }
286                         }
287
288                         break;
289
290                         //Check all tokens 5 chars long
291                 case 5:
292
293                         //Check for CAVOK
294                         if (!strcmp(token, "CAVOK")) {
295                                 res->cc = 1;
296                                 return;
297                         }
298
299                         //Check if token is the temperature
300                         for (i=0; i<2; i++) {
301                                 if (!isdigit(token[i])) break;
302                         }
303                         if ((i==2) && (token[2] == '/')) {
304                                 for (i=3; i<5; i++) {
305                                         if (!isdigit(token[i])) break;
306                                 }
307                                 if (i==5) {
308                                         //First 2 digits gives the air temperature
309                                         res->temp=atoi(token);
310
311                                         //4th and 5th digits gives the dew point temperature
312                                         res->dew=atoi(&token[3]);
313
314                                         //Compute humidity
315                                         res->hmid = rel_humidity(res->dew, res->temp);
316
317                                         return;
318                                 }
319                         }
320
321                         //Check if token is the pressure
322                         if ((token[0] == 'Q') || (token[0] == 'A')) {
323                                 for (i=1; i<5; i++) {
324                                         if (!isdigit(token[i])) break;
325                                 }
326                                 if (i==5) {
327                                         if (token[0] == 'A') {
328                                                 //Convert inches of mercury to mbar
329                                                 res->bar = (int)(atoi(&token[1])*0.338637526f);
330                                                 return;
331                                         }
332
333                                         //Last 4 digits is pressure im mbar
334                                         res->bar = atoi(&token[1]);
335                                         return;
336                                 }
337                         }
338
339                         //Check if token is a modified weather condition
340                         if ((token[0] == '+') || (token[0] == '-')) {
341                                 for(i=0; i<NUM_WM_CODES; i++) {
342                                         if (!strncmp(&token[1], WM_CODES[i], 2)) {
343                                                 for(i=0; i<NUM_WC_CODES; i++) {
344                                                         if (!strncmp(&token[3], WC_CODES[i], 2)) {
345                                                                 res->wc=i+1;
346                                                                 return;
347                                                         }
348                                                 }
349                                                 break;
350                                         }
351                                 }
352                         }
353                         break;
354
355                         //Check all tokens 6 chars long
356                 case 6:
357
358                         //Check if token is the cloud cover
359                         for (i=0; i<3; i++) {
360                                 if (!isalpha(token[i])) break;
361                         }
362                         if (i==3) {
363                                 for (i=3; i<6; i++) {
364                                         if (!isdigit(token[i])) break;
365                                 }
366                                 if (i==6) {
367                                         //Check if first 3 digits gives the cloud cover condition
368                                         for(i=0; i<NUM_CC_CODES; i++) {
369                                                 if (!strncmp(token, CC_CODES[i], 3)) {
370                                                         res->cc=i+1;
371                                                         break;
372                                                 }
373                                         }
374                                         return;
375                                 }
376                         }
377
378                         //Check if token is positive temp and negative dew
379                         for (i=0; i<2; i++) {
380                                 if (!isdigit(token[i])) break;
381                         }
382                         if ((i==2) && (token[2] == '/')  && (token[3] == 'M')) {
383                                 for (i=4; i<6; i++) {
384                                         if (!isdigit(token[i])) break;
385                                 }
386                                 if (i==6) {
387                                         //1st and 2nd digits gives the temperature
388                                         res->temp = atoi(token);
389
390                                         //5th and 6th digits gives the dew point temperature
391                                         res->dew = -atoi(&token[4]);
392
393                                         //Compute humidity
394                                         res->hmid = rel_humidity(res->dew, res->temp);
395
396                                         return;
397                                 }
398                         }
399
400                         break;
401
402                         //Check all tokens 7 chars long
403                 case 7:
404
405                         //Check if token is the observation time
406                         for (i=0; i<6; i++) {
407                                 if (!isdigit(token[i])) break;
408                         }
409                         if ((i==6) && (token[6] == 'Z')) return;
410
411                         //Check if token is the wind speed/direction in knots
412                         for (i=0; i<5; i++) {
413                                 if (!isdigit(token[i])) break;
414                         }
415                         if ((i==5) && (token[5] == 'K') &&  (token[6] == 'T')) {
416
417                                 //First 3 digits are wind direction
418                                 strncpy(s_tmp, token, 3);
419                                 s_tmp[3]='\0';
420                                 res->wind_d=atoi(s_tmp);
421
422                                 //4th and 5th digit are wind speed in knots (convert to km/hr)
423                                 res->wind_s = (int)(atoi(&token[3])*1.852);
424
425                                 return;
426                         }
427
428                         //Check if token is negative temperature
429                         if ((token[0] == 'M') && (token[4] == 'M')) {
430                                 for (i=1; i<3; i++) {
431                                         if (!isdigit(token[i])) break;
432                                 }
433                                 if ((i==3) && (token[3] == '/')) {
434                                         for (i=5; i<7; i++) {
435                                                 if (!isdigit(token[i])) break;
436                                         }
437                                         if (i==7) {
438                                                 //2nd and 3rd digits gives the temperature
439                                                 res->temp = -atoi(&token[1]);
440
441                                                 //6th and 7th digits gives the dew point temperature
442                                                 res->dew = -atoi(&token[5]);
443
444                                                 //Compute humidity
445                                                 res->hmid = rel_humidity(res->dew, res->temp);
446
447                                                 return;
448                                         }
449                                 }
450                         }
451
452                         //Check if token is wind variability
453                         for (i=0; i<3; i++) {
454                                 if (!isdigit(token[i])) break;
455                         }
456                         if ((i==3) && (token[3] == 'V')) {
457                                 for (i=4; i<7; i++) {
458                                         if (!isdigit(token[i])) break;
459                                 }
460                                 if (i==7) return;
461                         }
462
463                         break;
464
465                         //Check all tokens 8 chars long
466                 case 8:
467
468                         //Check if token is the wind speed/direction in m/s
469                         for (i=0; i<5; i++) {
470                                 if (!isdigit(token[i])) break;
471                         }
472                         if ((i==5)&&(token[5] == 'M')&&(token[6] == 'P')&&(token[7] == 'S')) {
473
474                                 //First 3 digits are wind direction
475                                 strncpy(s_tmp, token, 3);
476                                 s_tmp[3]='\0';
477                                 res->wind_d=atoi(s_tmp);
478
479                                 //4th and 5th digit are wind speed in m/s (convert to km/hr)
480                                 res->wind_s = (int)(atoi(&token[3])*3.6);
481
482                                 return;
483                         }
484
485                 default:
486
487                         //printf("token : %s\n", token);
488                         break;
489         }
490 }
491
492 static void parse_weather(PWEATHER *res, const char *data)
493 {
494         //Reset results
495         memset(res, 0, sizeof(PWEATHER));
496
497 #ifdef XOAP
498         //Check if it is an xml file
499         if ( strncmp(data, "<?xml ", 6) == 0 ) {
500                 parse_weather_xml(res, data);
501         } else
502 #endif /* XOAP */
503         {
504                 //We assume its a text file
505                 char s_tmp[256];
506                 const char delim[] = " ";
507
508                 //Divide time stamp and metar data
509                 if (sscanf(data, "%[^'\n']\n%[^'\n']", res->lastupd, s_tmp) == 2) {
510
511                         //Process all tokens
512                         char *p_tok = NULL;
513                         char *p_save = NULL;
514
515                         if ((strtok_r(s_tmp, delim, &p_save)) != NULL) {
516
517                                 //Jump first token, must be icao
518                                 p_tok = strtok_r(NULL, delim, &p_save);
519
520                                 do {
521
522                                         parse_token(res, p_tok);
523                                         p_tok = strtok_r(NULL, delim, &p_save);
524
525                                 } while (p_tok != NULL);
526                         }
527                         return;
528                 }
529                 else {
530                         return;
531                 }
532         }
533 }
534
535 void fetch_weather_info(location *curloc)
536 {
537         CURL *curl = NULL;
538         CURLcode res;
539
540         // curl temps
541         struct MemoryStruct chunk;
542
543         chunk.memory = NULL;
544         chunk.size = 0;
545
546         curl = curl_easy_init();
547         if (curl) {
548                 curl_easy_setopt(curl, CURLOPT_URL, curloc->uri);
549                 curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
550                 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
551                 curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &chunk);
552                 curl_easy_setopt(curl, CURLOPT_USERAGENT, "conky-weather/1.0");
553
554                 res = curl_easy_perform(curl);
555                 if (res == CURLE_OK && chunk.size) {
556                         timed_thread_lock(curloc->p_timed_thread);
557                         parse_weather(&curloc->data, chunk.memory);
558                         timed_thread_unlock(curloc->p_timed_thread);
559                         free(chunk.memory);
560                 } else {
561                         ERR("weather: no data from server");
562                 }
563
564                 curl_easy_cleanup(curl);
565         }
566
567         return;
568 }
569
570 void *weather_thread(void *) __attribute__((noreturn));
571
572 void init_thread(location *curloc, int interval)
573 {
574         curloc->p_timed_thread =
575                 timed_thread_create(&weather_thread,
576                                 (void *)curloc, interval * 1000000);
577         if (!curloc->p_timed_thread) {
578                 ERR("weather: error creating timed thread");
579         }
580         timed_thread_register(curloc->p_timed_thread,
581                         &curloc->p_timed_thread);
582         if (timed_thread_run(curloc->p_timed_thread)) {
583                 ERR("weather: error running timed thread");
584         }
585 }
586
587
588 void process_weather_info(char *p, int p_max_size, char *uri, char *data_type, int interval)
589 {
590         static const char *wc[] = {
591                 "", "drizzle", "rain", "hail", "soft hail",
592                 "snow", "snow grains", "fog", "haze", "smoke",
593                 "mist", "dust", "sand", "funnel cloud tornado",
594                 "dust/sand", "squall", "sand storm", "dust storm"
595         };
596
597         location *curloc = find_location(uri);
598         if (!curloc->p_timed_thread) init_thread(curloc, interval);
599
600         timed_thread_lock(curloc->p_timed_thread);
601         if (strcmp(data_type, "last_update") == EQUAL) {
602                 strncpy(p, curloc->data.lastupd, p_max_size);
603         } else if (strcmp(data_type, "temperature") == EQUAL) {
604                 temp_print(p, p_max_size, curloc->data.temp, TEMP_CELSIUS);
605         } else if (strcmp(data_type, "cloud_cover") == EQUAL) {
606 #ifdef XOAP
607                 if (curloc->data.xoap_t[0] != '\0') {
608                         strncpy(p, curloc->data.xoap_t, p_max_size);
609                 } else
610 #endif /* XOAP */
611                         if (curloc->data.cc == 0) {
612                                 strncpy(p, "", p_max_size);
613                         } else if (curloc->data.cc < 3) {
614                                 strncpy(p, "clear", p_max_size);
615                         } else if (curloc->data.cc < 5) {
616                                 strncpy(p, "partly cloudy", p_max_size);
617                         } else if (curloc->data.cc == 5) {
618                                 strncpy(p, "cloudy", p_max_size);
619                         } else if (curloc->data.cc == 6) {
620                                 strncpy(p, "overcast", p_max_size);
621                         } else if (curloc->data.cc == 7) {
622                                 strncpy(p, "towering cumulus", p_max_size);
623                         } else  {
624                                 strncpy(p, "cumulonimbus", p_max_size);
625                         }
626         } else if (strcmp(data_type, "pressure") == EQUAL) {
627                 snprintf(p, p_max_size, "%d", curloc->data.bar);
628         } else if (strcmp(data_type, "wind_speed") == EQUAL) {
629                 snprintf(p, p_max_size, "%d", curloc->data.wind_s);
630         } else if (strcmp(data_type, "wind_dir") == EQUAL) {
631                 if ((curloc->data.wind_d >= 349) || (curloc->data.wind_d < 12)) {
632                         strncpy(p, "N", p_max_size);
633                 } else if (curloc->data.wind_d < 33) {
634                         strncpy(p, "NNE", p_max_size);
635                 } else if (curloc->data.wind_d < 57) {
636                         strncpy(p, "NE", p_max_size);
637                 } else if (curloc->data.wind_d < 79) {
638                         strncpy(p, "ENE", p_max_size);
639                 } else if (curloc->data.wind_d < 102) {
640                         strncpy(p, "E", p_max_size);
641                 } else if (curloc->data.wind_d < 124) {
642                         strncpy(p, "ESE", p_max_size);
643                 } else if (curloc->data.wind_d < 147) {
644                         strncpy(p, "SE", p_max_size);
645                 } else if (curloc->data.wind_d < 169) {
646                         strncpy(p, "SSE", p_max_size);
647                 } else if (curloc->data.wind_d < 192) {
648                         strncpy(p, "S", p_max_size);
649                 } else if (curloc->data.wind_d < 214) {
650                         strncpy(p, "SSW", p_max_size);
651                 } else if (curloc->data.wind_d < 237) {
652                         strncpy(p, "SW", p_max_size);
653                 } else if (curloc->data.wind_d < 259) {
654                         strncpy(p, "WSW", p_max_size);
655                 } else if (curloc->data.wind_d < 282) {
656                         strncpy(p, "W", p_max_size);
657                 } else if (curloc->data.wind_d < 304) {
658                         strncpy(p, "WNW", p_max_size);
659                 } else if (curloc->data.wind_d < 327) {
660                         strncpy(p, "NW", p_max_size);
661                 } else if (curloc->data.wind_d < 349) {
662                         strncpy(p, "NNW", p_max_size);
663                 };
664         } else if (strcmp(data_type, "wind_dir_DEG") == EQUAL) {
665                 snprintf(p, p_max_size, "%d", curloc->data.wind_d);
666
667         } else if (strcmp(data_type, "humidity") == EQUAL) {
668                 snprintf(p, p_max_size, "%d", curloc->data.hmid);
669         } else if (strcmp(data_type, "weather") == EQUAL) {
670                 strncpy(p, wc[curloc->data.wc], p_max_size);
671         }
672
673         timed_thread_unlock(curloc->p_timed_thread);
674 }
675
676 void *weather_thread(void *arg)
677 {
678         location *curloc = (location*)arg;
679
680         while (1) {
681                 fetch_weather_info(curloc);
682                 if (timed_thread_test(curloc->p_timed_thread, 0)) {
683                         timed_thread_exit(curloc->p_timed_thread);
684                 }
685         }
686         /* never reached */
687 }
688