core/weather/weather_reading.c
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file weather_reading.c | ||
| 3 | * @brief Turns one weather message into the reading a store keeps. | ||
| 4 | * | ||
| 5 | * @ingroup lib_core | ||
| 6 | */ | ||
| 7 | #include "weather/weather_reading.h" | ||
| 8 | |||
| 9 | #include <limits.h> | ||
| 10 | |||
| 11 | #include "math/scale.h" | ||
| 12 | #include "text/cstring_fit.h" | ||
| 13 | |||
| 14 | #define SECONDS_PER_READING_HOUR 3600 ///< Seconds in an hour, for placing a strip's first column | ||
| 15 | #define SECONDS_PER_READING_DAY 86400 ///< Seconds in a day, for placing a strip's first day | ||
| 16 | #define MINUTES_PER_READING_DAY 1440 ///< Minutes in a day, the range a sunrise or sunset is held to | ||
| 17 | #define TEMP_LOWEST (-99) ///< The lowest temperature kept, the most a three digit label shows below zero | ||
| 18 | #define TEMP_HIGHEST 199 ///< The highest temperature kept, the most a three digit label shows | ||
| 19 | |||
| 20 | /** A number the message left out, as the store's own no-data value. */ | ||
| 21 | 18 | static int or_none(int value, int none) | |
| 22 | { | ||
| 23 |
2/2✓ Branch 0 taken 17 times.
✓ Branch 1 taken 1 time.
|
18 | return value == INT_MIN ? none : value; |
| 24 | } | ||
| 25 | |||
| 26 | /** | ||
| 27 | * A temperature held to TEMP_LOWEST to TEMP_HIGHEST, so a wild one cannot run past a three digit | ||
| 28 | * label or overflow the current temperature's int16, or no data for one the message left out. The | ||
| 29 | * store's own no-data value counts as left out too, since clamping it would turn it into a real -99. | ||
| 30 | * | ||
| 31 | * @param value The temperature from the message, or INT_MIN when it left one out. | ||
| 32 | * @return The temperature held to the range, or WEATHER_NO_TEMP. | ||
| 33 | */ | ||
| 34 | 22 | static int temp_or_none(int value) | |
| 35 | { | ||
| 36 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 5 times.
|
16 | return (value == INT_MIN || value == WEATHER_NO_TEMP) ? WEATHER_NO_TEMP |
| 37 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 6 times.
|
38 | : clamp_int(value, TEMP_LOWEST, TEMP_HIGHEST); |
| 38 | } | ||
| 39 | |||
| 40 | /** A time of day in minutes, or -1 for one the message left out or one no clock can read. */ | ||
| 41 | 4 | static int16_t day_minute_or_none(int value) | |
| 42 | { | ||
| 43 |
4/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 time.
✓ Branch 3 taken 1 time.
|
4 | return (value >= 0 && value < MINUTES_PER_READING_DAY) ? (int16_t)value : -1; |
| 44 | } | ||
| 45 | |||
| 46 | 19 | bool weather_reading_apply(WeatherState *state, const WeatherMessage *msg, const WeatherClock *clock) | |
| 47 | { | ||
| 48 | 19 | bool kept = false; | |
| 49 | |||
| 50 | // a failed fetch still answers, but its temperature is not a reading, so the last good one stays | ||
| 51 |
4/4✓ Branch 0 taken 5 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 time.
|
19 | if ((msg->groups & WEATHER_GROUP_CURRENT) && msg->ok) |
| 52 | { | ||
| 53 | 4 | state->temp = (int16_t)temp_or_none(msg->temp); | |
| 54 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
|
4 | cstring_fit(state->cond, msg->cond ? msg->cond : "--", sizeof(state->cond)); |
| 55 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
|
4 | cstring_fit(state->cond_label, msg->cond_label ? msg->cond_label : "", sizeof(state->cond_label)); |
| 56 | 4 | kept = true; | |
| 57 | } | ||
| 58 | |||
| 59 |
2/2✓ Branch 0 taken 2 times.
✓ Branch 1 taken 17 times.
|
19 | if (msg->groups & WEATHER_GROUP_EXTRA) |
| 60 | { | ||
| 61 | 2 | state->humidity = or_none(msg->humidity, -1); | |
| 62 | 2 | state->wind_kmh = or_none(msg->wind_kmh, -1); | |
| 63 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
|
2 | cstring_fit(state->wind_dir, msg->wind_dir ? msg->wind_dir : "", sizeof(state->wind_dir)); |
| 64 | 2 | state->sunrise = day_minute_or_none(msg->sunrise); | |
| 65 | 2 | state->sunset = day_minute_or_none(msg->sunset); | |
| 66 | 2 | kept = true; | |
| 67 | } | ||
| 68 | |||
| 69 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 14 times.
|
19 | if (msg->groups & WEATHER_GROUP_FORECAST) |
| 70 | { | ||
| 71 | 5 | state->uv = or_none(msg->uv, -1); | |
| 72 | 5 | state->temp_max = temp_or_none(msg->temp_max); | |
| 73 | 5 | state->temp_min = temp_or_none(msg->temp_min); | |
| 74 | 5 | state->precip_chance = or_none(msg->precip_chance, -1); | |
| 75 | 5 | state->forecast_day = clock->day_start; | |
| 76 | 5 | kept = true; | |
| 77 | } | ||
| 78 | |||
| 79 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 15 times.
|
19 | if (msg->groups & WEATHER_GROUP_AIR) |
| 80 | { | ||
| 81 | 4 | state->feels_like = temp_or_none(msg->feels_like); | |
| 82 | 4 | state->pressure = or_none(msg->pressure, -1); | |
| 83 | 4 | state->dew_point = temp_or_none(msg->dew_point); | |
| 84 | 4 | kept = true; | |
| 85 | } | ||
| 86 | |||
| 87 | // the decoders only write a strip that reads clean, so a bad one leaves the last good row | ||
| 88 | // each strip that reads clean records where its first column starts, which says later how much | ||
| 89 | // of it has gone by. the first hourly column is the hour with its base hour nearest now, up to | ||
| 90 | // 12 hours behind or 11 ahead. the first daily column is the day with its weekday nearest today, | ||
| 91 | // up to 3 days either way, so a late evening strip that starts tomorrow is whole until then. | ||
| 92 | // a strip that lands late starts behind the clock, and reading it as nearly a whole cycle ahead | ||
| 93 | // would mean it never ages | ||
| 94 |
4/4✓ Branch 0 taken 5 times.
✓ Branch 1 taken 14 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 2 times.
|
19 | if (msg->hourly && weather_hourly_decode(msg->hourly, msg->hourly_len, &state->hourly)) |
| 95 | { | ||
| 96 | 3 | int ahead = (state->hourly.base_hour - clock->hour + 36) % 24 - 12; | |
| 97 | 3 | state->hourly_first = clock->now - clock->minute * 60 - clock->second + | |
| 98 | 3 | (time_t)ahead * SECONDS_PER_READING_HOUR; | |
| 99 | 3 | kept = true; | |
| 100 | } | ||
| 101 | |||
| 102 |
3/4✓ Branch 0 taken 2 times.
✓ Branch 1 taken 17 times.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
|
19 | if (msg->daily && weather_daily_decode(msg->daily, msg->daily_len, &state->daily)) |
| 103 | { | ||
| 104 | 2 | int ahead = (state->daily.base_weekday - clock->wday + 10) % 7 - 3; | |
| 105 | 2 | state->daily_first = clock->day_start + (time_t)ahead * SECONDS_PER_READING_DAY; | |
| 106 | 2 | kept = true; | |
| 107 | } | ||
| 108 | |||
| 109 |
2/2✓ Branch 0 taken 16 times.
✓ Branch 1 taken 3 times.
|
19 | if (kept) |
| 110 | { | ||
| 111 | 16 | state->last_sync = clock->now; | |
| 112 | } | ||
| 113 | |||
| 114 | 19 | return kept; | |
| 115 | } | ||
| 116 | |||
| 117 | /** Divides to the nearest whole number. Neither a fifth nor a ninth lands on a half, so nothing ties. */ | ||
| 118 | 32 | static int div_nearest(int numerator, int denominator) | |
| 119 | { | ||
| 120 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 22 times.
|
32 | return (numerator + (numerator < 0 ? -denominator : denominator) / 2) / denominator; |
| 121 | } | ||
| 122 | |||
| 123 | /** One temperature in the other unit, or no data left as it is. */ | ||
| 124 | 33 | static int convert_temp(int value, bool to_fahrenheit) | |
| 125 | { | ||
| 126 |
2/2✓ Branch 0 taken 1 time.
✓ Branch 1 taken 32 times.
|
33 | if (value == WEATHER_NO_TEMP) |
| 127 | { | ||
| 128 | 1 | return value; | |
| 129 | } | ||
| 130 | |||
| 131 |
2/2✓ Branch 0 taken 22 times.
✓ Branch 1 taken 10 times.
|
32 | int converted = to_fahrenheit ? div_nearest(value * 9, 5) + 32 : div_nearest((value - 32) * 5, 9); |
| 132 | 32 | return clamp_int(converted, TEMP_LOWEST, TEMP_HIGHEST); | |
| 133 | } | ||
| 134 | |||
| 135 | 6 | void weather_reading_convert(WeatherState *state, bool to_fahrenheit) | |
| 136 | { | ||
| 137 | 6 | state->temp = (int16_t)convert_temp(state->temp, to_fahrenheit); | |
| 138 | 6 | state->temp_max = convert_temp(state->temp_max, to_fahrenheit); | |
| 139 | 6 | state->temp_min = convert_temp(state->temp_min, to_fahrenheit); | |
| 140 | 6 | state->feels_like = convert_temp(state->feels_like, to_fahrenheit); | |
| 141 | 6 | state->dew_point = convert_temp(state->dew_point, to_fahrenheit); | |
| 142 | |||
| 143 |
3/4✓ Branch 0 taken 1 time.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
7 | for (uint8_t i = 0; i < state->hourly.count && i < WEATHER_FORECAST_COLS; i++) |
| 144 | { | ||
| 145 | 1 | state->hourly.col[i].temp = (int16_t)convert_temp(state->hourly.col[i].temp, to_fahrenheit); | |
| 146 | } | ||
| 147 | |||
| 148 |
3/4✓ Branch 0 taken 1 time.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 1 time.
✗ Branch 3 not taken.
|
7 | for (uint8_t i = 0; i < state->daily.count && i < WEATHER_FORECAST_COLS; i++) |
| 149 | { | ||
| 150 | 1 | state->daily.col[i].temp_max = (int16_t)convert_temp(state->daily.col[i].temp_max, to_fahrenheit); | |
| 151 | 1 | state->daily.col[i].temp_min = (int16_t)convert_temp(state->daily.col[i].temp_min, to_fahrenheit); | |
| 152 | } | ||
| 153 | 6 | } | |
| 154 |