weather: add plugin

This plugin fetches weather information in the same fashion as
Xmobar's Weather plugin. It fetches from NOAA's website (by default)
using their standard format for reporting data from weather stations
around the world.

Also enable in CI.

TODO:

Fill out the example configuration in the docfile.
This commit is contained in:
Ben Boeckel 2025-02-08 19:16:08 +01:00
parent 082ab41598
commit 11f7f38a3c
12 changed files with 1187 additions and 0 deletions

View file

@ -27,6 +27,7 @@ packages:
- py3-pip - py3-pip
- flex - flex
- bison - bison
- curl-dev
sources: sources:
- https://git.sr.ht/~dnkl/yambar - https://git.sr.ht/~dnkl/yambar

View file

@ -32,6 +32,7 @@
* pipewire: added `spacing`, `left-spacing` and `right-spacing` * pipewire: added `spacing`, `left-spacing` and `right-spacing`
attributes. attributes.
* mpris: new module ([#53][53]). * mpris: new module ([#53][53]).
* weather: new module ([#448][448]).
[96]: https://codeberg.org/dnkl/yambar/issues/96 [96]: https://codeberg.org/dnkl/yambar/issues/96
[380]: https://codeberg.org/dnkl/yambar/issues/380 [380]: https://codeberg.org/dnkl/yambar/issues/380
@ -40,6 +41,7 @@
[428]: https://codeberg.org/dnkl/yambar/pulls/428 [428]: https://codeberg.org/dnkl/yambar/pulls/428
[404]: https://codeberg.org/dnkl/yambar/issues/404 [404]: https://codeberg.org/dnkl/yambar/issues/404
[53]: https://codeberg.org/dnkl/yambar/issues/53 [53]: https://codeberg.org/dnkl/yambar/issues/53
[448]: https://codeberg.org/dnkl/yambar/pulls/448
### Changed ### Changed

View file

@ -53,6 +53,9 @@ endif
if plugin_niri_workspaces_enabled if plugin_niri_workspaces_enabled
plugin_pages += ['yambar-modules-niri-workspaces.5.scd'] plugin_pages += ['yambar-modules-niri-workspaces.5.scd']
endif endif
if plugin_weather_enabled
plugin_pages += ['yambar-modules-weather.5.scd']
endif
if plugin_pipewire_enabled if plugin_pipewire_enabled
plugin_pages += ['yambar-modules-pipewire.5.scd'] plugin_pages += ['yambar-modules-pipewire.5.scd']
endif endif

View file

@ -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)

View file

@ -195,6 +195,7 @@ summary(
'Sway XKB keyboard': plugin_sway_xkb_enabled, 'Sway XKB keyboard': plugin_sway_xkb_enabled,
'Niri language': plugin_niri_language_enabled, 'Niri language': plugin_niri_language_enabled,
'Niri workspaces': plugin_niri_workspaces_enabled, 'Niri workspaces': plugin_niri_workspaces_enabled,
'Weather': plugin_weather_enabled,
'XKB keyboard (for X11)': plugin_xkb_enabled, 'XKB keyboard (for X11)': plugin_xkb_enabled,
'XWindow (window tracking for X11)': plugin_xwindow_enabled, 'XWindow (window tracking for X11)': plugin_xwindow_enabled,
}, },

View file

@ -50,6 +50,8 @@ option('plugin-niri-language', type: 'feature', value: 'auto',
description: 'language support for Niri') description: 'language support for Niri')
option('plugin-niri-workspaces', type: 'feature', value: 'auto', option('plugin-niri-workspaces', type: 'feature', value: 'auto',
description: 'workspaces support for Niri') description: 'workspaces support for Niri')
option('plugin-weather', type: 'feature', value: 'auto',
description: 'Weather polling support')
option('plugin-xkb', type: 'feature', value: 'auto', option('plugin-xkb', type: 'feature', value: 'auto',
description: 'keyboard support for X11') description: 'keyboard support for X11')
option('plugin-xwindow', type: 'feature', value: 'auto', option('plugin-xwindow', type: 'feature', value: 'auto',

View file

@ -60,6 +60,9 @@ plugin_xkb_enabled = backend_x11 and xcb_xkb.found()
plugin_xwindow_enabled = backend_x11 and get_option('plugin-xwindow').allowed() 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) # Module name -> (source-list, dep-list)
mod_data = {} 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]]} mod_data += {'niri-workspaces': [['niri-common.c', 'niri-common.h'], [dynlist, json_niri_workspaces]]}
endif 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 if plugin_xkb_enabled
mod_data += {'xkb': [[], [xcb_stuff, xcb_xkb]]} mod_data += {'xkb': [[], [xcb_stuff, xcb_xkb]]}
endif endif

341
modules/test-weather.c Normal file
View file

@ -0,0 +1,341 @@
#include "weather.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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, "<unknown>");
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;
}

408
modules/weather-parse.c Normal file
View file

@ -0,0 +1,408 @@
#include <ctype.h>
#include <float.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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("<unknown>");
free(info->station_state);
info->station_state = strdup("<unknown>");
info->year = 0;
info->month = 0;
info->day = 0;
info->hour = -1;
info->minute = -1;
free(info->wind_direction);
info->wind_direction = strdup("<unknown>");
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("<unknown>");
free(info->sky_condition);
info->sky_condition = strdup("<unknown>");
free(info->weather);
info->weather = strdup("<unknown>");
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;
}

243
modules/weather.c Normal file
View file

@ -0,0 +1,243 @@
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <poll.h>
#include <curl/curl.h>
#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

41
modules/weather.h Normal file
View file

@ -0,0 +1,41 @@
#pragma once
#include <stddef.h>
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);

View file

@ -93,6 +93,9 @@ EXTERN_MODULE(niri_language);
#if defined(HAVE_PLUGIN_niri_workspaces) #if defined(HAVE_PLUGIN_niri_workspaces)
EXTERN_MODULE(niri_workspaces); EXTERN_MODULE(niri_workspaces);
#endif #endif
#if defined(HAVE_PLUGIN_weather)
EXTERN_MODULE(weather);
#endif
#if defined(HAVE_PLUGIN_xkb) #if defined(HAVE_PLUGIN_xkb)
EXTERN_MODULE(xkb); EXTERN_MODULE(xkb);
#endif #endif
@ -232,6 +235,9 @@ static void __attribute__((constructor)) init(void)
#if defined(HAVE_PLUGIN_niri_workspaces) #if defined(HAVE_PLUGIN_niri_workspaces)
REGISTER_CORE_MODULE(niri-workspaces, niri_workspaces); REGISTER_CORE_MODULE(niri-workspaces, niri_workspaces);
#endif #endif
#if defined(HAVE_PLUGIN_weather)
REGISTER_CORE_MODULE(weather, weather);
#endif
#if defined(HAVE_PLUGIN_xkb) #if defined(HAVE_PLUGIN_xkb)
REGISTER_CORE_MODULE(xkb, xkb); REGISTER_CORE_MODULE(xkb, xkb);
#endif #endif