/* [ʞ] tenki.c * ~ lexi hale * > ./make.sh * © affero general public license * tenki is a client for the Dark Sky API. it is a * bail-early stream parser which retrieves a fixed set * of keys (this set may be easily changed in the code) * and prints them as a formatted string to stdout. it * was designed purely as a polybar plugin to display my * local weather, but it could be just as easily be * reworked for many other purposes * * you need to get an api key from dark sky before you * compile tenki, and place it in the file apikey - it's * segregated from the main code this way to allow it to * be excluded from version control. * * the default language of the results is Japanese; you * can change this by changing the macro ds_lang from * "ja" to your language code of choice. the ds_exclude * macro is used to minimize the bandwidth tenki uses by * instructing the dark sky server not to send those * fields back; if you want to access information in any * of them, you need to remove them from ds_exclude. * delete the #define entirely if you wish to access all * of the excluded fields. * * the function main() contains most of the code you * will likely wish to alter. it contains a variable * "paths" which is used to specify which fields of the * returned json object you wish the parser to extract. * each path is a "p-string of p-strings" - that is, its * first character is an integer which specifies the * depth of the path, and each "directory" is specified * with a string prefixed with the number of characters * in it. so the path "a/bc/def" would be encoded * "\3\1a\2bc\3def" - \3 for the three constituents "a" * "bc" and "def", \1 for the strlen of "a", \2 for the * strlen of "bc", etc. shoutout to the C standards * committee for forcing me to do this horrible thing. * * in conclusion, i should have written this in scheme. **/ #define ds_lang "ja" #define ds_exclude "minutely,hourly,daily,alerts,flags" // posix includes #include #include #include #include // libc includes #include #include // networking includes #include #include #include #include #ifndef ds_apikey # error missing api key; compile with make.sh # define ds_apikey "" // tidy up errors #endif #ifdef ds_exclude # define _http "?exclude=" ds_exclude "&" #else # define _http "?" #endif #define http _http "lang=" ds_lang " HTTP/1.1\r\nHost: api.darksky.net\r\n\r\n" #define start "GET /forecast/" ds_apikey "/" #define min(x,y) (x>y?y:x) #define len(x) (sizeof(x)/sizeof(*x)) typedef enum bool { false, true } bool; SSL_CTX* sc; bool head(size_t* len, char* txt, char** body) { char* hend = strstr(txt, "\r\n\r\n"); if (hend == NULL) return false; #define ctlhead "\r\nContent-Length:" char* ctl = strstr(txt, ctlhead); if (ctl == NULL || ctl > hend) return false; ctl += len(ctlhead); *len = 0; while(*ctl != '\r') { if (*ctl >= '0' && *ctl <= '9') *len*=10,*len+=*ctl-'0'; ++ctl; } *len+=(hend-txt)+4; *body = hend+4; return true; } size_t get(char* resp, size_t max, const char* msg, size_t total, char** body){ int port = 443; const char* host = "api.darksky.net"; //printf("req: %s\n", msg); struct hostent* api; struct sockaddr_in api_addr; int sockfd, bytes; size_t sent, recd; sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) exit(2); SSL *con = SSL_new(sc); SSL_set_fd(con, sockfd); api = gethostbyname(host); if (api == NULL) exit(3); memset(&api_addr, 0, sizeof(api_addr)); api_addr.sin_family = AF_INET; api_addr.sin_port = htons(port); memcpy(&api_addr.sin_addr.s_addr, api -> h_addr, api -> h_length); if (connect(sockfd,(struct sockaddr*)&api_addr,sizeof(api_addr))) exit(4); int err = SSL_connect(con); sent = 0; do { bytes = SSL_write(con,msg+sent,total-sent); if (bytes < 0) exit (4); if (bytes == 0) break; ++sent; } while (sent < total); recd = 0; size_t resplen = max; bool rhead = false; do { bytes = SSL_read(con,resp+recd,min(resplen-recd,15000)); if (bytes < 0) exit (5); if (bytes == 0) break; if(!rhead)rhead=head(&resplen, resp, body); recd += bytes; } while (recd < resplen - 1); close(sockfd); SSL_shutdown(con); return recd; } size_t pos(char* msg, const char* lat, const char* lng) { char* cur = msg + len(start) - 1; size_t latl = strlen(lat); size_t longl = strlen(lng); strcpy(cur, lat); cur += latl; *cur++=','; strcpy(cur, lng); cur += longl; strcpy(cur, http); cur += len(http); *cur--=0; return cur-msg; } bool estrc(size_t sz, const char* str1, const char* str2) { for(size_t i = 0;i keyend) return; if (str > end) return; /* print_path: { printf("current path: /"); for (size_t i = 0; i < depth; ++i) { printf("%.*s/",strchr(path[i],'"')-path[i],path[i]); } printf("\n"); printf("current key: "); printkey(*keys); } */ parse_char: { switch(*str++) { case ' ': case '\t': case '\n': goto parse_char; case '"': path[depth] = str; goto path_find_quote_end; case '}': if (depth == 0) return; depth--; while(*str!=',') ++str; ++str; goto init; default: printf("found illegal char %c\n", *(str-1)); exit(6); } } path_find_quote_end: { switch (*str++) { case '\\': ++str; goto path_find_quote_end; case '"': goto read_value; default: goto path_find_quote_end; } } read_value: { if (*str++!=':') { printf("illegal char\n"); exit(6); } bool iskey; const char* key = *keys; jsv* value; if (depth + 1 != *key++) { iskey = false; goto comp; } else for (size_t i = 0; i <= depth; ++i) { if (estrc(*key, key+1, path[i])) { key += *key + 1; } else { iskey = false; goto comp; } } iskey = true; value = values; ++keys; ++values; comp: if (*str == '{') { ++depth; ++str; goto init; } else if (*str == '"') { goto copy_str_value; } else if (*str == '[') { goto skip_array; } else if ((*str >= '0' && *str <= '9') || *str=='-') { goto copy_int_value; } else { printf("illegal character found in value %c\n", *str); exit(6); } copy_str_value: { fld = ++str; while (*++str != '"') if (*str == '\\') { ++str; continue; } if (iskey) { value -> s.sz = str - fld; value -> s.a = fld; } while (*++str != ','); ++str; goto init; } skip_array: { size_t ard = 0; skip_loop: switch (*++str) { case '{': case '[': ++ard; goto skip_loop; case '"': goto skip_str; case '}': if (ard == 0) { printf("bad json\n"); exit(7); } else { --ard; goto skip_loop; } case ']': if (ard == 0) { if (*++str == ',') { ++str; goto init; } else goto init; } else { --ard; } default: goto skip_loop; } skip_str: while (*str != '"') if (*str == '\\') { str += 2; continue; } else ++str; goto skip_loop; } copy_int_value: { if (!iskey) { while(*str++!=',') if (str > end) return; goto init; } value -> f = 0; bool neg; if (*str == '-') { neg=true; ++str; } else neg=false; while(*str >= '0' && *str <= '9') { value -> f *= 10; value -> f += *str - '0'; ++str; } if(*str == '.') { float fac = 0.1; float val = 0; while(*++str >= '0' && *str <= '9') { val += (((float)(*str - '0')) * fac); fac *= 0.1; } value -> f += val; } if(neg) value -> f *= -1; if (*str == ',') { ++str; goto init; } else { printf("illegal character %.*s\n",15,str); exit(6); } } } } } int main(int argc, char** argv) { if (argc != 3) exit(1); SSL_load_error_strings(); SSL_library_init(); sc = SSL_CTX_new(SSLv23_client_method()); char msg[256] = start; char resp[32368]; char* body; // name the fields to extract from the json stream // note: MUST BE LISTED IN ORDER THEY APPEAR // TODO: performance worth lack of flexibility? const char* paths[] = { "\2\x9""currently\7summary", "\2\x9""currently\xfprecipIntensity", "\2\x9""currently\xbtemperature", "\2\x9""currently\x9windspeed", }; //copy the latitude and longitude into the request size_t msgsz = pos(msg, argv[1], argv[2]); //msgsz = size of what we're going to transmit //for (;;) { size_t rspsz = get(resp, sizeof(resp), msg, msgsz, &body); // if paths contains the keys, this contains the values jsv values[len(paths)]; parse: { pplt(body, body + (rspsz - (resp - body)), paths, paths+len(paths), values); pstr summary = values[0].s; float pcpint = values[1].f, temp = values[2].f, windspd = values[3].f; printf("%.1f° %%{F#ff9bbb}%.*s\n", summary.sz, summary.a, temp); } // dark sky's API allows us to make 1000 free requests a day. // let's try not to get too near that. // hours in day 24 - minutes in day 24 * 60 = 1440 // so once a minute is almost half again too many // 1440 / 2 = 720 leaving reasonable refresh time without // going too near the limit. so we aim for one update // every two minutes. // sleep(60 * 2); // } return 0; }