diff --git a/.builds/alpine-x64.yml b/.builds/alpine-x64.yml index 1703a3d..b5c83f2 100644 --- a/.builds/alpine-x64.yml +++ b/.builds/alpine-x64.yml @@ -27,6 +27,7 @@ packages: - py3-pip - flex - bison + - curl-dev sources: - https://git.sr.ht/~dnkl/yambar diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e0e772..c1961c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ * pipewire: added `spacing`, `left-spacing` and `right-spacing` attributes. * mpris: new module ([#53][53]). +* weather: new module ([#448][448]). [96]: https://codeberg.org/dnkl/yambar/issues/96 [380]: https://codeberg.org/dnkl/yambar/issues/380 @@ -40,6 +41,7 @@ [428]: https://codeberg.org/dnkl/yambar/pulls/428 [404]: https://codeberg.org/dnkl/yambar/issues/404 [53]: https://codeberg.org/dnkl/yambar/issues/53 +[448]: https://codeberg.org/dnkl/yambar/pulls/448 ### Changed diff --git a/doc/meson.build b/doc/meson.build index e801bf1..eb5d31b 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -53,6 +53,9 @@ endif if plugin_niri_workspaces_enabled plugin_pages += ['yambar-modules-niri-workspaces.5.scd'] endif +if plugin_weather_enabled + plugin_pages += ['yambar-modules-weather.5.scd'] +endif if plugin_pipewire_enabled plugin_pages += ['yambar-modules-pipewire.5.scd'] endif diff --git a/doc/yambar-modules-weather.5.scd b/doc/yambar-modules-weather.5.scd new file mode 100644 index 0000000..31c7aa9 --- /dev/null +++ b/doc/yambar-modules-weather.5.scd @@ -0,0 +1,128 @@ +yambar-modules-weather(5) + +# NAME +weather - This module monitors weather station reports + +# DESCRIPTION + +This module monitors weather reports in NOAA's format from weather +stations around the world. It instantiates the provided _content_ +particle for the station specified. + +# TAGS + +[[ *Name* +:[ *Type* +:< *Description* +| station_town +: string +: Town name of the station. +| station_state +: string +: Region name of the station. +| year +: int +: Year of the weather report. +| month +: int +: Month of the weather report. +| day +: int +: Day of the weather report. +| hour +: int +: Hour of the weather report. +| minute +: int +: Minute of the weather report. +| wind_direction +: string +: Direction of the wind or *μ* if calm or variable. +| wind_azimuth +: int +: The azimuth of the wind or *-1* if calm or variable. +| wind_mph +: int +: The speed of the wind in miles per hour (m/h). +| wind_knots +: int +: The speed of the wind in nautical knots. +| wind_kmph +: int +: The speed of the wind in kilometers per hour (km/h). +| wind_mps +: int +: The speed of the wind in meters per second (m/s). +| visibility +: string +: Visibility report. +| sky_condition +: string +: Description of the sky condition (e.g., cloudy). +| weather +: string +: Description of the weather (not always available). +| temp_c +: float +: Temperature in Celsius. +| temp_f +: float +: Temperature in Fahrenheit. +| heat_index_c +: float +: Heat index in Celsius. +| heat_index_f +: float +: Heat index in Fahrenheit. +| dew_point_c +: float +: Dew point in Celsius. +| dew_point_f +: float +: Dew point in Fahrenheit. +| humidity +: int +: Relative humidity (0-100). +| pressure_mmhg +: float +: Pressure in millimeters of Mercury (mmHg). +| pressure_hpa +: int +: Pressure in hectapascals (hPa). + + +# CONFIGURATION + +[[ *Name* +:[ *Type* +:[ *Req* +:< *Description* +| station +: string +: yes +: Name of the station to report weather. +| host +: string +: no +: Website to fetch the weather report from. Defaults to NOAA's + website which uses ICAO names for stations. + + +# EXAMPLES + +Display weather report for KIAD (Washington Dulles Airport). + +``` +bar: + left: + - weather: + station: KIAD + content: + map: + TODO +``` + +# SEE ALSO + +*yambar-modules*(5), *yambar-particles*(5), *yambar-tags*(5), *yambar-decorations*(5) + diff --git a/meson.build b/meson.build index 67d3096..bd84c64 100644 --- a/meson.build +++ b/meson.build @@ -195,6 +195,7 @@ summary( 'Sway XKB keyboard': plugin_sway_xkb_enabled, 'Niri language': plugin_niri_language_enabled, 'Niri workspaces': plugin_niri_workspaces_enabled, + 'Weather': plugin_weather_enabled, 'XKB keyboard (for X11)': plugin_xkb_enabled, 'XWindow (window tracking for X11)': plugin_xwindow_enabled, }, diff --git a/meson_options.txt b/meson_options.txt index 23a8e11..463b939 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -50,6 +50,8 @@ option('plugin-niri-language', type: 'feature', value: 'auto', description: 'language support for Niri') option('plugin-niri-workspaces', type: 'feature', value: 'auto', description: 'workspaces support for Niri') +option('plugin-weather', type: 'feature', value: 'auto', + description: 'Weather polling support') option('plugin-xkb', type: 'feature', value: 'auto', description: 'keyboard support for X11') option('plugin-xwindow', type: 'feature', value: 'auto', diff --git a/modules/meson.build b/modules/meson.build index f6d53d8..33c2c48 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -60,6 +60,9 @@ plugin_xkb_enabled = backend_x11 and xcb_xkb.found() plugin_xwindow_enabled = backend_x11 and get_option('plugin-xwindow').allowed() +curl = dependency('libcurl', required: get_option('plugin-weather')) +plugin_weather_enabled = curl.found() and get_option('plugin-weather').allowed() + # Module name -> (source-list, dep-list) mod_data = {} @@ -144,6 +147,14 @@ if plugin_niri_workspaces_enabled mod_data += {'niri-workspaces': [['niri-common.c', 'niri-common.h'], [dynlist, json_niri_workspaces]]} endif +if plugin_weather_enabled + mod_data += {'weather': [['weather-parse.c'], [curl]]} + + e = executable('test-weather', ['test-weather.c', 'weather-parse.c'], + dependencies: [pixman, fcft, tllist, curl]) + test('module-weather', e) +endif + if plugin_xkb_enabled mod_data += {'xkb': [[], [xcb_stuff, xcb_xkb]]} endif diff --git a/modules/test-weather.c b/modules/test-weather.c new file mode 100644 index 0000000..1d5d266 --- /dev/null +++ b/modules/test-weather.c @@ -0,0 +1,341 @@ +#include "weather.h" + +#include +#include +#include + +#define PARSE_SUCCEED(name, marker, info, lit) \ + do { \ + if (0) { \ + puts("string literal assertion: " lit); \ + } \ + const struct weather_curl_buffer buf = { \ + .buffer = lit, \ + .capacity = 0, \ + .size = sizeof(lit), \ + }; \ + fprintf(stderr, "========================================\n"); \ + fprintf(stderr, "Testing " name "\n"); \ + int res = parse_weather_info(&buf, &info); \ + if (res) { \ + fprintf(stderr, "unexpected failure to parse test '" name "'\n"); \ + marker = 0; \ + overall = EXIT_FAILURE; \ + } else { \ + fprintf(stderr, "succeeded in parsing\n"); \ + } \ + } while (0) + +#define PARSE_FAIL(name, lit) \ + do { \ + if (0) { \ + puts("string literal assertion: " lit); \ + } \ + const struct weather_curl_buffer buf = { \ + .buffer = lit, \ + .capacity = 0, \ + .size = sizeof(lit), \ + }; \ + struct weather_info info = {0}; \ + fprintf(stderr, "========================================\n"); \ + fprintf(stderr, "Testing " name " (expected to fail)\n"); \ + int res = parse_weather_info(&buf, &info); \ + if (!res) { \ + fprintf(stderr, "unexpected successful parsing of '" name "'\n"); \ + overall = EXIT_FAILURE; \ + } else { \ + fprintf(stderr, "succeeded in detecting bad parsing\n"); \ + } \ + } while (0) + +#define CHECK_STR(info, field, expect) \ + do { \ + if (!info.field) { \ + fprintf(stderr, "Unexpected `" #field "`: NULL\n"); \ + } else if (strcmp(info.field, expect)) { \ + fprintf(stderr, "Unexpected `" #field "`: '%s', expected '" expect "'\n", info.field); \ + overall = EXIT_FAILURE; \ + } \ + } while (0) +#define CHECK_DOUBLE(info, field, expect) \ + do { \ + if (info.field != expect) { \ + fprintf(stderr, "Unexpected `" #field "`: '%f', expected '" #expect "'\n", info.field); \ + overall = EXIT_FAILURE; \ + } \ + } while (0) +#define CHECK_INT(info, field, expect) \ + do { \ + if (info.field != expect) { \ + fprintf(stderr, "Unexpected `" #field "`: '%d', expected '" #expect "'\n", info.field); \ + overall = EXIT_FAILURE; \ + } \ + } while (0) + +#define DEFAULT_STATION \ + "Tirstrup, Denmark (EKAH) 56-18N 010-37E 25M\n" +#define CHECK_DEFAULT_STATION(info) \ + do { \ + CHECK_STR(info, station_town, "Tirstrup"); \ + CHECK_STR(info, station_state, "Denmark"); \ + } while (0) +#define DEFAULT_TIME \ + "Dec 22, 2024 - 04:20 PM EST / 2024.12.22 2120 UTC\n" +#define CHECK_DEFAULT_TIME(info) \ + do { \ + CHECK_INT(info, year, 2024); \ + CHECK_INT(info, month, 12); \ + CHECK_INT(info, day, 22); \ + CHECK_INT(info, hour, 21); \ + CHECK_INT(info, minute, 20); \ + } while (0) +#define DEFAULT_WIND \ + "Wind: from the SSW (200 degrees) at 5 MPH (4 KT) (direction variable):0\n" +#define CHECK_DEFAULT_WIND(info) \ + do { \ + CHECK_STR(info, wind_direction, "SSW"); \ + CHECK_INT(info, wind_azimuth, 200); \ + CHECK_INT(info, wind_mph, 5); \ + CHECK_INT(info, wind_knots, 4); \ + CHECK_INT(info, wind_kmph, 7); \ + CHECK_INT(info, wind_mps, 2); \ + } while (0) +#define DEFAULT_VISIBILITY \ + "Visibility: greater than 7 mile(s):0\n" +#define CHECK_DEFAULT_VISIBILITY(info) \ + do { \ + CHECK_STR(info, visibility, "greater than 7 mile(s)"); \ + } while (0) +#define DEFAULT_SKY_CONDITIONS \ + "Sky conditions: mostly clear\n" +#define CHECK_DEFAULT_SKY_CONDITIONS(info) \ + do { \ + CHECK_STR(info, sky_condition, "mostly clear"); \ + } while (0) +#define DEFAULT_WEATHER \ + "Weather: rain\n" +#define CHECK_DEFAULT_WEATHER(info) \ + do { \ + CHECK_STR(info, weather, "rain"); \ + } while (0) +#define DEFAULT_TEMPERATURE \ + "Temperature: 37 F (3 C)\n" +#define CHECK_DEFAULT_TEMPERATURE(info) \ + do { \ + CHECK_DOUBLE(info, temp_f, 37); \ + CHECK_DOUBLE(info, temp_c, 3); \ + } while (0) +#define DEFAULT_HEAT_INDEX \ + "Heat index: 35 F (2 C):0\n" +#define CHECK_DEFAULT_HEAT_INDEX(info) \ + do { \ + CHECK_DOUBLE(info, heat_index_f, 35); \ + CHECK_DOUBLE(info, heat_index_c, 2); \ + } while (0) +#define DEFAULT_DEW_POINT \ + "Dew Point: 35 F (2 C)\n" +#define CHECK_DEFAULT_DEW_POINT(info) \ + do { \ + CHECK_DOUBLE(info, dew_point_f, 35); \ + CHECK_DOUBLE(info, dew_point_c, 2); \ + } while (0) +#define DEFAULT_RELATIVE_HUMIDITY \ + "Relative Humidity: 93%\n" +#define CHECK_DEFAULT_RELATIVE_HUMIDITY(info) \ + do { \ + CHECK_INT(info, humidity, 93); \ + } while (0) +#define DEFAULT_PRESSURE \ + "Pressure (altimeter): 29.26 in. Hg (0991 hPa)\n" +#define CHECK_DEFAULT_PRESSURE(info) \ + do { \ + CHECK_DOUBLE(info, pressure_mmhg, 29.26); \ + CHECK_INT(info, pressure_hpa, 991); \ + } while (0) +#define DEFAULT_FOOTER \ + "ob: EKAH 222120Z AUTO 20004KT 150V250 9999 FEW140/// 03/02 Q0991\n" \ + "cycle: 21\n" + +int main(int argc, char* argv[]) +{ + int overall = EXIT_SUCCESS; + + PARSE_FAIL("empty string", ""); + + // Default contents parsing + { + int parse_ok = 1; + struct weather_info info = {0}; + PARSE_SUCCEED("default values", + parse_ok, info, + DEFAULT_STATION + DEFAULT_TIME + DEFAULT_WIND + DEFAULT_VISIBILITY + DEFAULT_SKY_CONDITIONS + DEFAULT_WEATHER + DEFAULT_TEMPERATURE + DEFAULT_HEAT_INDEX + DEFAULT_DEW_POINT + DEFAULT_RELATIVE_HUMIDITY + DEFAULT_PRESSURE + DEFAULT_FOOTER); + if (parse_ok) { + CHECK_DEFAULT_STATION(info); + CHECK_DEFAULT_TIME(info); + CHECK_DEFAULT_WIND(info); + CHECK_DEFAULT_VISIBILITY(info); + CHECK_DEFAULT_WEATHER(info); + CHECK_DEFAULT_SKY_CONDITIONS(info); + CHECK_DEFAULT_TEMPERATURE(info); + CHECK_DEFAULT_HEAT_INDEX(info); + CHECK_DEFAULT_DEW_POINT(info); + CHECK_DEFAULT_RELATIVE_HUMIDITY(info); + CHECK_DEFAULT_PRESSURE(info); + } + } + + // Unknown station parsing + { + int parse_ok = 1; + struct weather_info info = {0}; + PARSE_SUCCEED("unknown station", + parse_ok, info, + "Station name not available\n" + DEFAULT_TIME + DEFAULT_WIND + DEFAULT_VISIBILITY + DEFAULT_SKY_CONDITIONS + DEFAULT_WEATHER + DEFAULT_TEMPERATURE + DEFAULT_HEAT_INDEX + DEFAULT_DEW_POINT + DEFAULT_RELATIVE_HUMIDITY + DEFAULT_PRESSURE + DEFAULT_FOOTER); + if (parse_ok) { + CHECK_STR(info, station_town, "?"); + CHECK_STR(info, station_state, "?"); + CHECK_DEFAULT_TIME(info); + CHECK_DEFAULT_WIND(info); + CHECK_DEFAULT_VISIBILITY(info); + CHECK_DEFAULT_WEATHER(info); + CHECK_DEFAULT_SKY_CONDITIONS(info); + CHECK_DEFAULT_TEMPERATURE(info); + CHECK_DEFAULT_HEAT_INDEX(info); + CHECK_DEFAULT_DEW_POINT(info); + CHECK_DEFAULT_RELATIVE_HUMIDITY(info); + CHECK_DEFAULT_PRESSURE(info); + } + } + + // Missing weather parsing. + { + int parse_ok = 1; + struct weather_info info = {0}; + PARSE_SUCCEED("missing_weather", + parse_ok, info, + DEFAULT_STATION + DEFAULT_TIME + DEFAULT_WIND + DEFAULT_VISIBILITY + DEFAULT_SKY_CONDITIONS + DEFAULT_TEMPERATURE + DEFAULT_HEAT_INDEX + DEFAULT_DEW_POINT + DEFAULT_RELATIVE_HUMIDITY + DEFAULT_PRESSURE + DEFAULT_FOOTER); + if (parse_ok) { + CHECK_DEFAULT_STATION(info); + CHECK_DEFAULT_TIME(info); + CHECK_DEFAULT_WIND(info); + CHECK_DEFAULT_VISIBILITY(info); + CHECK_STR(info, weather, ""); + CHECK_DEFAULT_SKY_CONDITIONS(info); + CHECK_DEFAULT_TEMPERATURE(info); + CHECK_DEFAULT_HEAT_INDEX(info); + CHECK_DEFAULT_DEW_POINT(info); + CHECK_DEFAULT_RELATIVE_HUMIDITY(info); + CHECK_DEFAULT_PRESSURE(info); + } + } + + // Calm wind parsing + { + int parse_ok = 1; + struct weather_info info = {0}; + PARSE_SUCCEED("calm wind", + parse_ok, info, + DEFAULT_STATION + DEFAULT_TIME + "Wind: Calm:0\n" + DEFAULT_VISIBILITY + DEFAULT_SKY_CONDITIONS + DEFAULT_WEATHER + DEFAULT_TEMPERATURE + DEFAULT_HEAT_INDEX + DEFAULT_DEW_POINT + DEFAULT_RELATIVE_HUMIDITY + DEFAULT_PRESSURE + DEFAULT_FOOTER); + if (parse_ok) { + CHECK_DEFAULT_STATION(info); + CHECK_DEFAULT_TIME(info); + CHECK_STR(info, wind_direction, "μ"); + CHECK_INT(info, wind_azimuth, -1); + CHECK_INT(info, wind_mph, 0); + CHECK_INT(info, wind_knots, 0); + CHECK_INT(info, wind_kmph, 0); + CHECK_INT(info, wind_mps, 0); + CHECK_DEFAULT_VISIBILITY(info); + CHECK_DEFAULT_WEATHER(info); + CHECK_DEFAULT_SKY_CONDITIONS(info); + CHECK_DEFAULT_TEMPERATURE(info); + CHECK_DEFAULT_HEAT_INDEX(info); + CHECK_DEFAULT_DEW_POINT(info); + CHECK_DEFAULT_RELATIVE_HUMIDITY(info); + CHECK_DEFAULT_PRESSURE(info); + } + } + + // Variable wind parsing + { + int parse_ok = 1; + struct weather_info info = {0}; + PARSE_SUCCEED("variable wind", + parse_ok, info, + DEFAULT_STATION + DEFAULT_TIME + "Wind: Variable at 5 MPH (4 KT)\n" + DEFAULT_VISIBILITY + DEFAULT_SKY_CONDITIONS + DEFAULT_WEATHER + DEFAULT_TEMPERATURE + DEFAULT_HEAT_INDEX + DEFAULT_DEW_POINT + DEFAULT_RELATIVE_HUMIDITY + DEFAULT_PRESSURE + DEFAULT_FOOTER); + if (parse_ok) { + CHECK_DEFAULT_STATION(info); + CHECK_DEFAULT_TIME(info); + CHECK_STR(info, wind_direction, "μ"); + CHECK_INT(info, wind_azimuth, -1); + CHECK_INT(info, wind_mph, 5); + CHECK_INT(info, wind_knots, 4); + CHECK_INT(info, wind_kmph, 7); + CHECK_INT(info, wind_mps, 2); + CHECK_DEFAULT_VISIBILITY(info); + CHECK_DEFAULT_WEATHER(info); + CHECK_DEFAULT_SKY_CONDITIONS(info); + CHECK_DEFAULT_TEMPERATURE(info); + CHECK_DEFAULT_HEAT_INDEX(info); + CHECK_DEFAULT_DEW_POINT(info); + CHECK_DEFAULT_RELATIVE_HUMIDITY(info); + CHECK_DEFAULT_PRESSURE(info); + } + } + + return overall; +} diff --git a/modules/weather-parse.c b/modules/weather-parse.c new file mode 100644 index 0000000..038b9a7 --- /dev/null +++ b/modules/weather-parse.c @@ -0,0 +1,408 @@ +#include +#include +#include +#include +#include +#include + +#include "weather.h" + +#define check_pos_impl(p, op, m) \ + do { \ + if ((p) op (m)) { \ + fprintf(stderr, "failed to parse at %d\n", __LINE__); \ + parse_fail = 1; \ + } \ + } while (0) +#define check_parse(end, pos) check_pos_impl(end, !=, pos) + +static int +parse_line(struct weather_info *info, const char* line, size_t len) +{ + const char wind_prefix[] = "Wind: "; + const char visibility_prefix[] = "Visibility: "; + const char sky_conditions_prefix[] = "Sky conditions: "; + const char weather_prefix[] = "Weather: "; + const char temperature_prefix[] = "Temperature: "; + const char heat_index_prefix[] = "Heat index: "; + const char dew_point_prefix[] = "Dew Point: "; + const char relative_humidity_prefix[] = "Relative Humidity: "; + const char pressure_prefix[] = "Pressure (altimeter): "; + + const char* lend = line + len; + int parse_fail = 0; + +#define has_prefix(l, s, p) ((sizeof(p) - 1) <= s && !strncmp(l, p, sizeof(p) - 1)) + + // Wind: ... + if (has_prefix(line, len, wind_prefix)) { + line += sizeof(wind_prefix) - 1; + len -= sizeof(wind_prefix) - 1; + + const char calm_indicator[] = "Calm:0"; + const char variable_indicator[] = "Variable at "; + const char normal_indicator[] = "from the "; + + // Wind: Calm:0 + if (has_prefix(line, len, calm_indicator)) { + info->wind_direction = strdup("μ"); + info->wind_azimuth = -1; + info->wind_mph = 0; + info->wind_knots = 0; + info->wind_kmph = 0; + info->wind_mps = 0; + // Wind: Variable at N MPH (N KT) + } else if (has_prefix(line, len, variable_indicator)) { + line += sizeof(variable_indicator) - 1; + len -= sizeof(variable_indicator) - 1; + + info->wind_direction = strdup("μ"); + info->wind_azimuth = -1; + + size_t spn = strcspn(line, " "); + char* end = NULL; + long long int_parse = strtoll(line, &end, 10); + check_parse(end, line + spn); + line += spn + 1; + check_pos_impl(line, >=, lend); + + info->wind_mph = int_parse; + + spn = strcspn(line, "("); + line += spn + 1; + check_pos_impl(line, >=, lend); + + int_parse = strtoll(line, &end, 10); + line = end; + check_pos_impl(line, >=, lend); + + info->wind_knots = int_parse; + info->wind_kmph = int_parse * 1.852; + info->wind_mps = int_parse * 0.514; + // Wind: from the DIR (AZI degrees) at N MPH (N KT) ... + } else if (has_prefix(line, len, normal_indicator)) { + line += sizeof(normal_indicator) - 1; + len -= sizeof(normal_indicator) - 1; + + size_t spn = strcspn(line, " "); + check_pos_impl(line + spn + 2, >=, lend); + info->wind_direction = strndup(line, spn); + + line += spn + 2; + check_pos_impl(line, >=, lend); + + spn = strcspn(line, " "); + char* end = NULL; + long long int_parse = strtoll(line, &end, 10); + check_parse(end, line + spn); + + info->wind_azimuth = int_parse; + + spn = strcspn(line, "t"); + line += spn + 2; + check_pos_impl(line, >=, lend); + + spn = strcspn(line, " "); + int_parse = strtoll(line, &end, 10); + check_parse(end, line + spn); + + info->wind_mph = int_parse; + + spn = strcspn(line, "("); + line += spn + 1; + check_pos_impl(line, >=, lend); + + spn = strcspn(line, " "); + int_parse = strtoll(line, &end, 10); + check_parse(end, line + spn); + + info->wind_knots = int_parse; + info->wind_kmph = int_parse * 1.852; + info->wind_mps = int_parse * 0.514; + } else { + fprintf(stderr, "Unknown 'Wind' format: '%.*s'\n", (int)len, line); + parse_fail = 1; + } + // Visibility: VIS + } else if (has_prefix(line, len, visibility_prefix)) { + line += sizeof(visibility_prefix) - 1; + len -= sizeof(visibility_prefix) - 1; + + size_t spn = strcspn(line, ":"); + check_pos_impl(line + spn, >=, lend); + + free(info->visibility); + info->visibility = strndup(line, spn); + // Sky conditions: SKY + } else if (has_prefix(line, len, sky_conditions_prefix)) { + line += sizeof(sky_conditions_prefix) - 1; + len -= sizeof(sky_conditions_prefix) - 1; + + size_t spn = strcspn(line, "\n"); + check_pos_impl(line + spn, >, lend); + + free(info->sky_condition); + info->sky_condition = strndup(line, spn); + // Weather: WEATHER + } else if (has_prefix(line, len, weather_prefix)) { + line += sizeof(weather_prefix) - 1; + len -= sizeof(weather_prefix) - 1; + + size_t spn = strcspn(line, "\n"); + check_pos_impl(line + spn, >, lend); + + free(info->weather); + info->weather = strndup(line, spn); + // Temperature: T F (T C) + } else if (has_prefix(line, len, temperature_prefix)) { + line += sizeof(temperature_prefix) - 1; + len -= sizeof(temperature_prefix) - 1; + + size_t spn = strcspn(line, " "); + char* end = NULL; + double double_parse = strtod(line, &end); + check_parse(end, line + spn); + line += spn + 1; + check_pos_impl(line, >=, lend); + + info->temp_f = double_parse; + + spn = strcspn(line, "("); + line += spn + 1; + check_pos_impl(line, >=, lend); + + spn = strcspn(line, " "); + double_parse = strtod(line, &end); + check_parse(end, line + spn); + + info->temp_c = double_parse; + // Heat index: T F (T C):N + } else if (has_prefix(line, len, heat_index_prefix)) { + line += sizeof(heat_index_prefix) - 1; + len -= sizeof(heat_index_prefix) - 1; + + size_t spn = strcspn(line, " "); + char* end = NULL; + double double_parse = strtod(line, &end); + check_parse(end, line + spn); + line += spn + 1; + check_pos_impl(line, >=, lend); + + info->heat_index_f = double_parse; + + spn = strcspn(line, "("); + line += spn + 1; + check_pos_impl(line, >=, lend); + + spn = strcspn(line, " "); + double_parse = strtod(line, &end); + check_parse(end, line + spn); + + info->heat_index_c = double_parse; + // Dew Point: T F (T C) + } else if (has_prefix(line, len, dew_point_prefix)) { + line += sizeof(dew_point_prefix) - 1; + len -= sizeof(dew_point_prefix) - 1; + + size_t spn = strcspn(line, " "); + char* end = NULL; + double double_parse = strtod(line, &end); + check_parse(end, line + spn); + line += spn + 1; + check_pos_impl(line, >=, lend); + + info->dew_point_f = double_parse; + + spn = strcspn(line, "("); + line += spn + 1; + check_pos_impl(line, >=, lend); + + spn = strcspn(line, " "); + double_parse = strtod(line, &end); + check_parse(end, line + spn); + + info->dew_point_c = double_parse; + // Relative Humidity: H% + } else if (has_prefix(line, len, relative_humidity_prefix)) { + line += sizeof(relative_humidity_prefix) - 1; + len -= sizeof(relative_humidity_prefix) - 1; + + size_t spn = strcspn(line, "%"); + char* end = NULL; + long long int_parse = strtoll(line, &end, 10); + check_parse(end, line + spn); + line += spn + 1; + check_pos_impl(line, >, lend); + + info->humidity = int_parse; + // Pressure (altimeter): X.Y in. Hg (N hPa) + } else if (has_prefix(line, len, pressure_prefix)) { + line += sizeof(pressure_prefix) - 1; + len -= sizeof(pressure_prefix) - 1; + + size_t spn = strcspn(line, " "); + char* end = NULL; + double double_parse = strtod(line, &end); + check_parse(end, line + spn); + line += spn + 1; + check_pos_impl(line, >, lend); + + info->pressure_mmhg = double_parse; + + spn = strcspn(line, "("); + line += spn + 1; + check_pos_impl(line, >=, lend); + + spn = strcspn(line, " "); + long long int_parse = strtoll(line, &end, 10); + check_parse(end, line + spn); + + info->pressure_hpa = int_parse; + } + + return parse_fail; +} + +int +parse_weather_info(const struct weather_curl_buffer *buf, struct weather_info *info) +{ + // Extract station information. + const char* pos = buf->buffer; + int parse_fail = 0; +#define check_pos() check_pos_impl(pos, >, buf->buffer + buf->size) + + // Fill in invalid data. + free(info->station_town); + info->station_town = strdup(""); + free(info->station_state); + info->station_state = strdup(""); + info->year = 0; + info->month = 0; + info->day = 0; + info->hour = -1; + info->minute = -1; + free(info->wind_direction); + info->wind_direction = strdup(""); + info->wind_azimuth = -1; + info->wind_mph = -1; + info->wind_knots = -1; + info->wind_kmph = -1; + info->wind_mps = -1; + free(info->visibility); + info->visibility = strdup(""); + free(info->sky_condition); + info->sky_condition = strdup(""); + free(info->weather); + info->weather = strdup(""); + info->temp_c = -1000; + info->temp_f = -1000; + info->heat_index_c = -1000; + info->heat_index_f = -1000; + info->dew_point_c = -1000; + info->dew_point_f = -1000; + info->humidity = -1; + info->pressure_mmhg = -1; + info->pressure_hpa = -1; + + // TOWN, STATE (... + if (!parse_fail) { + size_t spn; + + const char *unknown_station = "Station name not available"; + + if (!strncmp(pos, unknown_station, sizeof(unknown_station) - 1)) { + free(info->station_town); + info->station_town = strdup("?"); + free(info->station_state); + info->station_state = strdup("?"); + } else { + spn = strcspn(pos, ","); + free(info->station_town); + info->station_town = strndup(pos, spn); + pos += spn + 1; + check_pos(); + + if (!parse_fail) { + spn = strcspn(pos, "("); + // Trim trailing space + while (isspace(pos[spn - 1])) { + --spn; + } + // Skip leading space + while (isspace(*pos)) { + ++pos; + --spn; + } + free(info->station_state); + info->station_state = strndup(pos, spn); + } + } + + // Move to the end of the line. + spn = strcspn(pos, "\n"); + pos += spn + 1; + check_pos(); + } + // Dec 22, 2024 - 04:20 PM EST / 2024.12.22 2120 UTC + if (!parse_fail) { + size_t spn; + const char *lpos = pos; + char *end; + long int_parse; + + // Work on the entire line. + spn = strcspn(pos, "\n"); + pos += spn + 1; + check_pos(); + + spn = strcspn(lpos, "/"); + lpos += spn + 2; + check_pos_impl(lpos, >=, pos); + + int_parse = strtoll(lpos, &end, 10); + check_parse(end, lpos + 4); + lpos = end + 1; + check_pos_impl(lpos, >=, pos); + + info->year = int_parse; + + int_parse = strtoll(lpos, &end, 10); + check_parse(end, lpos + 2); + lpos = end + 1; + check_pos_impl(lpos, >=, pos); + + info->month = int_parse; + + int_parse = strtoll(lpos, &end, 10); + check_parse(end, lpos + 2); + lpos = end + 1; + check_pos_impl(lpos, >=, pos); + + info->day = int_parse; + + info->hour = (*lpos - '0') * 10 + lpos[1] - '0'; + lpos += 2; + check_pos_impl(lpos, >=, pos); + info->minute = (*lpos - '0') * 10 + lpos[1] - '0'; + } + + while (*pos && pos < buf->buffer + buf->size) { + size_t spn; + const char *lpos = pos; + + // Work on the entire line. + spn = strcspn(pos, "\n"); + pos += spn; + // Handle contents without newlines at the end of the file. + if (*pos == '\n') { + ++pos; + } + + if (parse_line(info, lpos, spn)) { + fprintf(stderr, "failed to parse line '%.*s'\n", (int)spn, lpos); + parse_fail = 1; + } + } + + return parse_fail; +} diff --git a/modules/weather.c b/modules/weather.c new file mode 100644 index 0000000..5aeb2cf --- /dev/null +++ b/modules/weather.c @@ -0,0 +1,243 @@ +#include +#include +#include +#include + +#include + +#include + +#define LOG_MODULE "weather" +#define LOG_ENABLE_DBG 0 +#include "../bar/bar.h" +#include "../config-verify.h" +#include "../config.h" +#include "../log.h" +#include "../plugin.h" +#include "version.h" + +#include "weather.h" + +#define DEFAULT_HOST "https://tgftp.nws.noaa.gov/data/observations/metar/decoded/" + +static void +free_weather_info(struct weather_info *info) +{ + if (!info) { + return; + } + free(info->station_town); + free(info->station_state); + free(info->wind_direction); + free(info->visibility); + free(info->sky_condition); + free(info->weather); + free(info); +} + +struct private +{ + struct particle *label; + char *host; + char *station; + + struct weather_info *info; +}; + +static void +destroy(struct module *mod) +{ + struct private *m = mod->private; + m->label->destroy(m->label); + free(m->host); + free(m->station); + free_weather_info(m->info); + free(m); + module_default_destroy(mod); +} + +static const char * +description(const struct module *mod) +{ + return "weather"; +} + +static struct exposable * +content(struct module *mod) +{ + const struct private *m = mod->private; + + mtx_lock(&mod->lock); + + struct tag_set tags = { + .tags = (struct tag *[]){ + tag_new_string(mod, "station_town", m->info->station_town), + tag_new_string(mod, "station_state", m->info->station_state), + tag_new_int(mod, "year", m->info->year), + tag_new_int(mod, "month", m->info->month), + tag_new_int(mod, "day", m->info->day), + tag_new_int(mod, "hour", m->info->hour), + tag_new_int(mod, "minute", m->info->minute), + tag_new_string(mod, "wind_direction", m->info->wind_direction), + tag_new_int(mod, "wind_azimuth", m->info->wind_azimuth), + tag_new_int(mod, "wind_mph", m->info->wind_mph), + tag_new_int(mod, "wind_knots", m->info->wind_knots), + tag_new_int(mod, "wind_kmph", m->info->wind_kmph), + tag_new_int(mod, "wind_mps", m->info->wind_mps), + tag_new_string(mod, "visibility", m->info->visibility), + tag_new_string(mod, "sky_condition", m->info->sky_condition), + tag_new_string(mod, "weather", m->info->weather), + tag_new_float(mod, "temp_c", m->info->temp_c), + tag_new_float(mod, "temp_f", m->info->temp_f), + tag_new_float(mod, "heat_index_c", m->info->heat_index_c), + tag_new_float(mod, "heat_index_f", m->info->heat_index_f), + tag_new_float(mod, "dew_point_c", m->info->dew_point_c), + tag_new_float(mod, "dew_point_f", m->info->dew_point_f), + tag_new_int(mod, "humidity", m->info->humidity), + tag_new_float(mod, "pressure_mmhg", m->info->pressure_mmhg), + tag_new_int(mod, "pressure_hpa", m->info->pressure_hpa), + }, + .count = 25, + }; + + mtx_unlock(&mod->lock); + + struct exposable *exposable = m->label->instantiate(m->label, &tags); + + tag_set_destroy(&tags); + return exposable; +} + +static size_t +weather_curl_buffer_write(void *contents, size_t size, size_t n, void *data) +{ + size_t new_size = size * n; + struct weather_curl_buffer *buf = (struct weather_curl_buffer *)data; + + // Grow the buffer if necessary. + size_t needed = buf->size + new_size + 1; + if (buf->capacity < needed) { + size_t new_cap = buf->capacity; + while (new_cap < needed) { + new_cap <<= 1; + } + char *newmem = realloc(buf->buffer, new_cap); + if (!newmem) { + // TODO: log + return 0; + } + + buf->buffer = newmem; + buf->capacity = new_cap; + } + + memcpy(buf->buffer + buf->size, contents, new_size); + buf->size += new_size; + buf->buffer[buf->size] = '\0'; + + return new_size; +}; + +struct weather_curl_buffer * +weather_curl_buffer_new() +{ + struct weather_curl_buffer *buf = (struct weather_curl_buffer*)calloc(1, sizeof(*buf)); + // Start with one page of memory. + buf->buffer = calloc(1, 4096); + buf->capacity = buf->buffer ? 4096 : 0; + + return buf; +} + +void +free_weather_curl_buffer(struct weather_curl_buffer *buf) +{ + if (!buf) { + return; + } + free(buf->buffer); + free(buf); +} + +static int +run(struct module *mod) +{ + struct private *m = mod->private; + free_weather_info(m->info); + m->info = calloc(1, sizeof(*m->info)); + + static char url[4096]; + snprintf(url, sizeof(url), "%s%s.TXT", m->host, m->station); + + CURL *curl = curl_easy_init(); + struct weather_curl_buffer *buf = weather_curl_buffer_new(); + if (curl) { + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, weather_curl_buffer_write); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "yambar/" YAMBAR_VERSION); + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + free_weather_curl_buffer(buf); + buf = NULL; + } + } + + curl_easy_cleanup(curl); + + if (!buf) { + return 1; + } + + int ret = parse_weather_info(buf, m->info); + free_weather_curl_buffer(buf); + + return ret; +} + +static struct module * +weather_new(struct particle *label, const char* host, const char *station) +{ + struct private *m = calloc(1, sizeof(*m)); + m->label = label; + m->host = strdup(host); + m->station = strdup(station); + + struct module *mod = module_common_new(); + mod->private = m; + mod->run = &run; + mod->destroy = &destroy; + mod->content = &content; + mod->description = &description; + return mod; +} + +static struct module * +from_conf(const struct yml_node *node, struct conf_inherit inherited) +{ + const struct yml_node *c = yml_get_value(node, "content"); + const struct yml_node *station = yml_get_value(node, "station"); + const struct yml_node *host = yml_get_value(node, "host"); + + return weather_new(conf_to_particle(c, inherited), yml_value_as_string(station), host == NULL ? DEFAULT_HOST : yml_value_as_string(host)); +} + +static bool +verify_conf(keychain_t *chain, const struct yml_node *node) +{ + static const struct attr_info attrs[] = { + {"station", true, &conf_verify_string}, + {"host", false, &conf_verify_string}, + MODULE_COMMON_ATTRS, + }; + + return conf_verify_dict(chain, node, attrs); +} + +const struct module_iface module_weather_iface = { + .verify_conf = &verify_conf, + .from_conf = &from_conf, +}; + +#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES) +extern const struct module_iface iface __attribute__((weak, alias("module_weather_iface"))); +#endif diff --git a/modules/weather.h b/modules/weather.h new file mode 100644 index 0000000..b04d3a6 --- /dev/null +++ b/modules/weather.h @@ -0,0 +1,41 @@ +#pragma once +#include + +struct weather_info +{ + char* station_town; + char* station_state; + int year; + int month; + int day; + int hour; + int minute; + char* wind_direction; + int wind_azimuth; + int wind_mph; + int wind_knots; + int wind_kmph; + int wind_mps; + char* visibility; + char* sky_condition; + char* weather; + double temp_c; + double temp_f; + double heat_index_c; + double heat_index_f; + double dew_point_c; + double dew_point_f; + int humidity; + double pressure_mmhg; + int pressure_hpa; +}; + +struct weather_curl_buffer +{ + char *buffer; + size_t size; + size_t capacity; +}; + +int +parse_weather_info(const struct weather_curl_buffer *buf, struct weather_info *info); diff --git a/plugin.c b/plugin.c index 2ed0a4f..2801a93 100644 --- a/plugin.c +++ b/plugin.c @@ -93,6 +93,9 @@ EXTERN_MODULE(niri_language); #if defined(HAVE_PLUGIN_niri_workspaces) EXTERN_MODULE(niri_workspaces); #endif +#if defined(HAVE_PLUGIN_weather) +EXTERN_MODULE(weather); +#endif #if defined(HAVE_PLUGIN_xkb) EXTERN_MODULE(xkb); #endif @@ -232,6 +235,9 @@ static void __attribute__((constructor)) init(void) #if defined(HAVE_PLUGIN_niri_workspaces) REGISTER_CORE_MODULE(niri-workspaces, niri_workspaces); #endif +#if defined(HAVE_PLUGIN_weather) + REGISTER_CORE_MODULE(weather, weather); +#endif #if defined(HAVE_PLUGIN_xkb) REGISTER_CORE_MODULE(xkb, xkb); #endif