Host C Specs


Directory: src/c/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 64 / 0 / 64
Functions: 100.0% 7 / 0 / 7
Branches: 90.0% 45 / 0 / 50

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