From e1146fe006d8c7b3a1e791b3bcfb53e9af7d4eb9 Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Tue, 2 Apr 2024 00:20:35 -0700 Subject: [PATCH] Add stat range menu --- Makefile | 16 ++-- src/main.c | 2 + src/ui/datapoints.c | 13 ++-- src/ui/datesel.c | 24 +++++- src/ui/datesel.h | 13 +++- src/ui/statrange.c | 173 ++++++++++++++++++++++++++++++++++++++++++++ src/ui/statrange.h | 38 ++++++++++ src/ui/statsby.c | 23 ++---- src/ui/timesel.c | 121 +++++++++++++++++++++++-------- src/ui/timesel.h | 18 ++++- src/util.c | 136 +++++++++++++++++++++------------- src/util.h | 25 +++++-- 12 files changed, 474 insertions(+), 128 deletions(-) create mode 100644 src/ui/statrange.c create mode 100644 src/ui/statrange.h diff --git a/Makefile b/Makefile index f574b9b..58a2505 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,8 @@ CFLAGS=-g -std=c11 -Wall ${SQLITE3_CFLAGS} LD=clang LDFLAGS=-lgpio -lfigpar -lpthread ${SQLITE3_LDFLAGS} SRCS=src/main.c src/util.c src/lcd.c src/ths.c src/button.c src/ui/screen.c\ - src/ui/datesel.c src/ui/statsby.c src/ui/datapoints.c src/ui/timesel.c + src/ui/datesel.c src/ui/statsby.c src/ui/datapoints.c src/ui/timesel.c\ + src/ui/statrange.c PROG=rpi4b-temp-humidity OBJS=${SRCS:C/^src/bin/:C/.c$/.o/} @@ -15,24 +16,29 @@ bin/${PROG}: ${OBJS} bin/main.o bin/util.o bin/lcd.o bin/ths.o bin/button.o: src/util.h bin/ui/screen.o bin/ui/statsby.o bin/ui/datesel.o: src/util.h -bin/ui/datapoints.o bin/ui/timesel.o: src/util.h +bin/ui/datapoints.o bin/ui/timesel.o bin/ui/statrange.o: src/util.h bin/main.o bin/lcd.o bin/screen.o bin/ui/datesel.o: src/lcd.h -bin/ui/statsby.o bin/ui/timesel.o: src/lcd.h +bin/ui/statsby.o bin/ui/timesel.o bin/ui/statrange.o: src/lcd.h +bin/ui/datapoints.o: src/lcd.h bin/main.o bin/ui/screen.o bin/ui/statsby.o: src/ui/screen.h bin/ui/datesel.o bin/ui/datapoints.o bin/ui/timesel.o: src/ui/screen.h +bin/ui/statrange.o: src/ui/screen.h bin/main.o bin/ui/datesel.o bin/ui/statsby.o: src/ui/datesel.h -bin/ui/datapoints.o bin/ui/timesel.o: src/ui/datesel.h +bin/ui/datapoints.o bin/ui/timesel.o bin/ui/statrange.o: src/ui/datesel.h bin/main.o bin/ths.o bin/ui/screen.o bin/ui/statsby.o: src/ths.h -bin/ui/timesel.o: src/ths.h +bin/ui/timesel.o src/ui/statrange.o: src/ths.h bin/main.o bin/ui/timesel.o bin/ui/datapoints.o: src/ui/timesel.h +bin/ui/statrange.o: src/ui/timesel.h + bin/main.o bin/button.o bin/ui/screen.o: src/button.h bin/main.o bin/ui/statsby.o: src/ui/statsby.h bin/main.o bin/ui/datapoints.o: src/ui/datapoints.h +bin/main.o bin/ui/statrange.o: src/ui/statrange.h ${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/} @mkdir -p ${.TARGET:H} diff --git a/src/main.c b/src/main.c index 0a3a178..367e537 100644 --- a/src/main.c +++ b/src/main.c @@ -15,6 +15,7 @@ #include "ui/screen.h" #include "ui/statsby.h" #include "ui/datapoints.h" +#include "ui/statrange.h" #include #include @@ -105,6 +106,7 @@ int main(int argc, char *const *argv) { screen_manager_add(screen_manager, (Screen *) stats_screen_new()); screen_manager_add(screen_manager, (Screen *) stats_by_screen_new()); screen_manager_add(screen_manager, (Screen *) data_points_screen_new()); + screen_manager_add(screen_manager, (Screen *) stat_range_screen_new()); while (RUNNING) { lock_stat_globals(); uint32_t temp = LAST_TEMP; diff --git a/src/ui/datapoints.c b/src/ui/datapoints.c index 486fba6..a63d59e 100644 --- a/src/ui/datapoints.c +++ b/src/ui/datapoints.c @@ -21,8 +21,7 @@ static bool data_points_select_date(DataPointsScreen *screen, } if (screen->need_redraw) { screen->need_redraw = false; - DateSelectionState dss = date_sel_dispatch(&screen->ds, state, - PERIOD_DAY, "Date:"); + DateSelectionState dss = date_sel_dispatch(&screen->ds, state, "Date:"); switch (dss) { case DATE_SEL_CONTINUE: break; // ignore @@ -33,6 +32,8 @@ static bool data_points_select_date(DataPointsScreen *screen, case DATE_SEL_BACK: case DATE_SEL_ERROR: screen->need_redraw = true; + date_sel_reset(&screen->ds); + time_sel_reset(&screen->ts); return true; } } @@ -65,7 +66,7 @@ static void data_points_select_time(DataPointsScreen *screen, } if (screen->need_redraw) { screen->need_redraw = false; - TimeSelState tss = time_sel_dispatch(&screen->ts, state); + TimeSelState tss = time_sel_dispatch(&screen->ts, state, "Time:"); switch (tss) { case TIME_SEL_CONTINUE: break; // ignore @@ -85,6 +86,8 @@ static void data_points_select_time(DataPointsScreen *screen, static void data_points_show(DataPointsScreen *screen, SensorState *state) { if (state->back_down) { screen->stage = DP_STAGE_DATE; + screen->ds.stage = DATE_SEL_YEAR; + screen->ts.stage = TS_STAGE_HOUR; screen->need_redraw = true; return; } @@ -158,7 +161,7 @@ DataPointsScreen *data_points_screen_new() { (ScreenCleanupFunc) free); s->need_redraw = true; s->stage = DP_STAGE_DATE; - date_sel_init(&s->ds, DATE_SEL_DAY); - time_sel_init(&s->ts, &s->ds); + date_sel_init(&s->ds, PERIOD_DAY); + time_sel_init(&s->ts, &s->ds, false); return s; } diff --git a/src/ui/datesel.c b/src/ui/datesel.c index 6e6f9c1..8d02e6d 100644 --- a/src/ui/datesel.c +++ b/src/ui/datesel.c @@ -12,10 +12,10 @@ #include #include -void date_sel_init(DateSelection *ds, DateSelectionStage max_stage) { - ds->max_stage = max_stage; +void date_sel_init(DateSelection *ds, UtilPeriod limit_period) { memset(&ds->start_time, 0, sizeof(UtilDate)); // min date memset(&ds->end_time, 0xff, sizeof(UtilDate)); // max date + date_sel_set_period(ds, limit_period); date_sel_reset(ds); } @@ -24,6 +24,23 @@ void date_sel_reset(DateSelection *ds) { ds->stage = DATE_SEL_YEAR; } +void date_sel_set_period(DateSelection *ds, UtilPeriod limit_period) { + ds->limit_period = limit_period; + switch (limit_period) { + case PERIOD_HOUR: + case PERIOD_DAY: + case PERIOD_WEEK: + ds->max_stage = DATE_SEL_DAY; + break; + case PERIOD_MONTH: + ds->max_stage = DATE_SEL_MONTH; + break; + case PERIOD_YEAR: + ds->max_stage = DATE_SEL_YEAR; + break; + } +} + static void date_sel_clamp_time(DateSelection *ds) { if (ds->year > ds->end_time.local_year) { ds->year = ds->end_time.local_year; @@ -99,10 +116,9 @@ static void date_sel_add_units(DateSelection *ds, int n) { DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state, - UtilPeriod limit_period, const char *label) { UtilDate start, end; - if (!get_database_limits(state->db, limit_period, &start, &end)) { + if (!get_database_limits(state->db, ds->limit_period, &start, &end)) { warnx("failed to query database limits"); lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); diff --git a/src/ui/datesel.h b/src/ui/datesel.h index 23e5b91..ef90269 100644 --- a/src/ui/datesel.h +++ b/src/ui/datesel.h @@ -25,6 +25,7 @@ typedef struct { int day; DateSelectionStage stage; DateSelectionStage max_stage; + UtilPeriod limit_period; UtilDate start_time; UtilDate end_time; } DateSelection; @@ -37,23 +38,27 @@ typedef enum { } DateSelectionState; /* - * Initialize a DateSelection. MAX_STAGE is the most specific field - * to select. + * Initialize a DateSelection. LIMIT_PERIOD is the most specific field + * to select and the period by which to select. */ -void date_sel_init(DateSelection *ds, DateSelectionStage max_stage); +void date_sel_init(DateSelection *ds, UtilPeriod limit_period); /* * Reset the date on DS. */ void date_sel_reset(DateSelection *ds); +/* + * Set the LIMIT_PERIOD for DS. + */ +void date_sel_set_period(DateSelection *ds, UtilPeriod limit_period); + /* * Dispatch a DateSelection by processing STATE to continue the selection. * Return: weather the user backed out, is still working, or is done */ DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state, - UtilPeriod limit_period, const char *label); #endif diff --git a/src/ui/statrange.c b/src/ui/statrange.c new file mode 100644 index 0000000..0a02666 --- /dev/null +++ b/src/ui/statrange.c @@ -0,0 +1,173 @@ +/* + * statrange.c - Screen that displays stats between two times + * Copyright (C) 2024 Alexander Rosenberg + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See the LICENSE file for more information. + */ +#include "statrange.h" +#include "../ths.h" + +#include +#include +#include + +// return true to go back +static bool stat_range_handle_date(StatRangeScreen *screen, DateSelection *ds, + SensorState *state, const char *label) { + if (state->down_down || state->up_down || state->back_down || + state->sel_down) { + screen->need_redraw = true; + } + if (screen->need_redraw) { + screen->need_redraw = false; + DateSelectionState res = date_sel_dispatch(ds, state, label); + switch (res) { + case DATE_SEL_CONTINUE: + break; // ignore + case DATE_SEL_DONE: + ++screen->stage; + screen->need_redraw = true; + break; + case DATE_SEL_BACK: + case DATE_SEL_ERROR: + screen->need_redraw = true; + return true; + } + } + return false; +} + +static void stat_range_handle_time(StatRangeScreen *screen, TimeSelection *ts, + SensorState *state, const char *label) { + if (state->down_down || state->up_down || state->back_down || + state->sel_down) { + screen->need_redraw = true; + } + if (screen->need_redraw) { + screen->need_redraw = false; + TimeSelState res = time_sel_dispatch(ts, state, label); + switch (res) { + case TIME_SEL_CONTINUE: + break; // ignore + case TIME_SEL_DONE: + ++screen->stage; + screen->need_redraw = true; + break; + case TIME_SEL_BACK: + case TIME_SEL_ERROR: + screen->need_redraw = true; + --screen->stage; + break; + } + } +} + +static uint64_t to_uts_timestamp(DateSelection *ds, TimeSelection *ts) { + struct tm broken_time = { + .tm_sec = ts->second, + .tm_min = ts->minute, + .tm_hour = ts->hour, + .tm_mday = ds->day, + .tm_mon = ds->month - 1, + .tm_year = ds->year - 1900, + .tm_isdst = -1, + }; + time_t it = mktime(&broken_time); + if (it < 0) { + warnx("failed to convert to UTC timestamp"); + return 0; + } + return it; +} + +static void stat_range_show_data(StatRangeScreen *screen, SensorState *state) { + if (state->back_down) { + screen->need_redraw = true; + screen->sds.stage = DATE_SEL_YEAR; + screen->sts.stage = TS_STAGE_HOUR; + screen->eds.stage = DATE_SEL_YEAR; + screen->ets.stage = TS_STAGE_HOUR; + screen->stage = STAT_RANGE_START_DATE; + return; + } + if (screen->need_redraw) { + screen->need_redraw = false; + uint64_t start = to_uts_timestamp(&screen->sds, &screen->sts); + uint64_t end = to_uts_timestamp(&screen->eds, &screen->ets); + if (start > end) { + uint64_t t = end; + end = start; + start = t; + } + UtilAverageRange data; + if (!get_average_for_range(state->db, start, end, &data)) { + warnx("could not get average for range: %" PRIu64 "-%" PRIu64, + start, end); + --screen->stage; + return; + } + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + char points_line[17]; + snprintf(points_line, 17, "%" PRIi64 "pts:", data.npoints); + lcd_write_string(state->lcd, points_line); + lcd_move_to(state->lcd, 1, 0); + if (data.npoints) { + char data_line[17]; + snprintf(data_line, 17, "%.1fF %d%%", DK_TO_F(data.temp), data.humid); + lcd_write_string(state->lcd, data_line); + } else { + lcd_write_string(state->lcd, "No data!"); + } + } +} + +static bool stat_range_screen_dispatch(StatRangeScreen *screen, + SensorState *state) { + if (state->force_draw) { + screen->need_redraw = true; + } + switch (screen->stage) { + case STAT_RANGE_START_DATE: + if(stat_range_handle_date(screen, &screen->sds, state, "Start Date:")) { + date_sel_reset(&screen->sds); + time_sel_reset(&screen->sts); + date_sel_reset(&screen->eds); + time_sel_reset(&screen->ets); + return true; + } + break; + case STAT_RANGE_START_TIME: + stat_range_handle_time(screen, &screen->sts, state, "Start Time:"); + break; + case STAT_RANGE_END_DATE: + if (stat_range_handle_date(screen, &screen->eds, state, "End Date:")) { + --screen->stage; + } + break; + case STAT_RANGE_END_TIME: + stat_range_handle_time(screen, &screen->ets, state, "End Time:"); + break; + case STAT_RANGE_SHOW: + stat_range_show_data(screen, state); + break; + } + return false; +} + +StatRangeScreen *stat_range_screen_new(void) { + StatRangeScreen *s = malloc_checked(sizeof(StatRangeScreen)); + screen_init(&s->parent, "Average range", + (ScreenDispatchFunc) stat_range_screen_dispatch, + (ScreenCleanupFunc) free); + s->need_redraw = true; + s->stage = STAT_RANGE_START_DATE; + date_sel_init(&s->sds, PERIOD_DAY); + time_sel_init(&s->sts, &s->sds, true); + date_sel_init(&s->eds, PERIOD_DAY); + time_sel_init(&s->ets, &s->eds, true); + return s; +} diff --git a/src/ui/statrange.h b/src/ui/statrange.h new file mode 100644 index 0000000..c99e62c --- /dev/null +++ b/src/ui/statrange.h @@ -0,0 +1,38 @@ +/* + * statrange.h - Screen that displays stats between two times + * Copyright (C) 2024 Alexander Rosenberg + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. See the LICENSE file for more information. + */ +#ifndef INCLUDED_STATRANGE_H +#define INCLUDED_STATRANGE_H + +#include "datesel.h" +#include "timesel.h" + +typedef struct { + Screen parent; + enum { + STAT_RANGE_START_DATE, + STAT_RANGE_START_TIME, + STAT_RANGE_END_DATE, + STAT_RANGE_END_TIME, + STAT_RANGE_SHOW + } stage; + bool need_redraw; + DateSelection sds; + TimeSelection sts; + DateSelection eds; + TimeSelection ets; +} StatRangeScreen; + +/* + * Create a new stat range screen. The screen will display average data between + * two times. + */ +StatRangeScreen *stat_range_screen_new(void); + +#endif diff --git a/src/ui/statsby.c b/src/ui/statsby.c index ca579d5..026b406 100644 --- a/src/ui/statsby.c +++ b/src/ui/statsby.c @@ -37,19 +37,7 @@ static void stats_by_select_period(StatsByScreen *screen, SensorState *state) { } if (state->sel_down) { ++screen->stage; - switch (screen->period) { - case PERIOD_HOUR: - case PERIOD_DAY: - case PERIOD_WEEK: - screen->ds.max_stage = DATE_SEL_DAY; - break; - case PERIOD_MONTH: - screen->ds.max_stage = DATE_SEL_MONTH; - break; - case PERIOD_YEAR: - screen->ds.max_stage = DATE_SEL_YEAR; - break; - } + date_sel_set_period(&screen->ds, screen->period); screen->need_redraw = true; } } @@ -60,8 +48,7 @@ static void stats_by_select_start(StatsByScreen *screen, SensorState *state) { } if (screen->need_redraw) { screen->need_redraw = false; - DateSelectionState dss = date_sel_dispatch(&screen->ds, state, - screen->period, "Start:"); + DateSelectionState dss = date_sel_dispatch(&screen->ds, state, "Start:"); switch (dss) { case DATE_SEL_CONTINUE: break; // ignore @@ -99,8 +86,8 @@ static void stats_by_show_stats(StatsByScreen *screen, SensorState *state) { screen->ds.day == screen->ds.start_time.local_day) { screen->offset_scale = screen->ds.start_time.local_hour; } - UtilAverageRange data; - if (!get_average_for_range(state->db, screen->ds.year, screen->ds.month, + UtilAveragePeriod data; + if (!get_average_for_period(state->db, screen->ds.year, screen->ds.month, screen->ds.day, screen->offset_scale, screen->period, &data)) { lcd_write_string(state->lcd, "Query failed!"); @@ -190,6 +177,6 @@ StatsByScreen *stats_by_screen_new() { s->need_redraw = true; s->stage = STATS_BY_SELECT_PERIOD; s->period = PERIOD_HOUR; - date_sel_init(&s->ds, DATE_SEL_DAY); + date_sel_init(&s->ds, PERIOD_DAY); return s; } diff --git a/src/ui/timesel.c b/src/ui/timesel.c index 6e0aec1..3804b45 100644 --- a/src/ui/timesel.c +++ b/src/ui/timesel.c @@ -11,11 +11,10 @@ #include -void time_sel_init(TimeSelection *ts, DateSelection *ds) { - ts->hour = 0; - ts->minute = 0; - ts->hour_set = false; +void time_sel_init(TimeSelection *ts, DateSelection *ds, bool do_second) { ts->ds = ds; + ts->do_second = do_second; + time_sel_reset(ts); } static void time_sel_clamp(TimeSelection *ts) { @@ -30,6 +29,12 @@ static void time_sel_clamp(TimeSelection *ts) { ts->minute > ts->ds->end_time.local_minute) { ts->minute = ts->ds->end_time.local_minute; } + + if (ts->hour == ts->ds->end_time.local_hour && + ts->minute == ts->ds->end_time.local_minute && + ts->second > ts->ds->end_time.local_second) { + ts->second = ts->ds->end_time.local_second; + } } else if (ts->ds->start_time.local_year == ts->ds->year && ts->ds->start_time.local_month == ts->ds->month && ts->ds->start_time.local_day == ts->ds->day) { @@ -40,34 +45,57 @@ static void time_sel_clamp(TimeSelection *ts) { ts->minute < ts->ds->start_time.local_minute) { ts->minute = ts->ds->start_time.local_minute; } + if (ts->hour == ts->ds->start_time.local_hour && + ts->minute == ts->ds->start_time.local_minute && + ts->second > ts->ds->start_time.local_second) { + ts->second = ts->ds->start_time.local_second; + } } } } static void time_sel_add(TimeSelection *ts, int amount) { - if (!ts->hour_set) { - ts->hour = (ts->hour + amount) % 24; - } else { - if (ts->minute + amount >= 60) { - ts->hour = (ts->hour + 1) % 24; - } else if (ts->minute + amount < 0) { - ts->hour = (ts->hour - 1) % 24; - } - ts->minute = (ts->minute + amount) % 60; + switch (ts->stage) { + case TS_STAGE_HOUR: + ts->hour += amount; + break; + case TS_STAGE_MINUTE: + ts->minute += amount; + break; + case TS_STAGE_SECOND: + ts->second += amount; + break; } + if (ts->second >= 60) { + ++ts->minute; + } else if (ts->second < 0) { + --ts->minute; + } + if (ts->minute >= 60) { + ++ts->hour; + } else if (ts->minute < 0) { + --ts->hour; + } + ts->hour %= 24; + ts->minute %= 60; + ts->second %= 60; if (ts->hour < 0) { - ts->hour = 24 + ts->hour; + ts->hour += 24; } if (ts->minute < 0) { - ts->minute = 60 + ts->minute; + ts->minute += 60; + } + if (ts->second < 0) { + ts->second += 60; } time_sel_clamp(ts); } -TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state) { +TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state, + const char *label) { if (state->back_down) { - if (ts->hour_set) { - ts->hour_set = false; + if (ts->stage != TS_STAGE_HOUR) { + --ts->stage; } else { lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); @@ -75,12 +103,13 @@ TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state) { } } if (state->sel_down) { - if (!ts->hour_set) { - ts->hour_set = true; - } else { + if ((ts->do_second && ts->stage == TS_STAGE_SECOND) || + (!ts->do_second && ts->stage == TS_STAGE_MINUTE)) { lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, LCD_DISPLAY_ON); return TIME_SEL_DONE; + } else { + ++ts->stage; } } if (ts->ds) { @@ -97,18 +126,40 @@ TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state) { time_sel_add(ts, state->up_down - state->down_down); lcd_clear(state->lcd); lcd_move_to(state->lcd, 0, 0); - lcd_write_string(state->lcd, "Time:"); - const char *format; - int cursor_pos; - if (!ts->hour_set) { - format = ">%02d:%02d"; - cursor_pos = 0; - } else { - format = "%02d:>%02d"; - cursor_pos = 3; - } + lcd_write_string(state->lcd, label); char time_line[17]; - snprintf(time_line, 17, format, ts->hour, ts->minute); + int cursor_pos; + if (ts->do_second) { + const char *format; + switch (ts->stage) { + case TS_STAGE_HOUR: + format = ">%02d:%02d:%02d"; + cursor_pos = 0; + break; + case TS_STAGE_MINUTE: + format = "%02d:>%02d:%02d"; + cursor_pos = 3; + break; + case TS_STAGE_SECOND: + format = "%02d:%02d:>%02d"; + cursor_pos = 6; + break; + } + snprintf(time_line, 17, format, ts->hour, ts->minute, ts->second); + } else { + const char *format; + switch (ts->stage) { + case TS_STAGE_HOUR: + format = ">%02d:%02d"; + cursor_pos = 0; + break; + default: // minute or second (which shouldn't happen) + format = "%02d:>%02d"; + cursor_pos = 3; + break; + } + snprintf(time_line, 17, format, ts->hour, ts->minute); + } lcd_move_to(state->lcd, 1, 0); lcd_write_string(state->lcd, time_line); lcd_move_to(state->lcd, 1, cursor_pos); @@ -116,3 +167,9 @@ TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state) { LCD_DISPLAY_ON); return TIME_SEL_CONTINUE; } + +void time_sel_reset(TimeSelection *ts) { + ts->hour = 0; + ts->minute = 0; + ts->stage = TS_STAGE_HOUR; +} diff --git a/src/ui/timesel.h b/src/ui/timesel.h index a045c3b..1a18577 100644 --- a/src/ui/timesel.h +++ b/src/ui/timesel.h @@ -16,9 +16,15 @@ typedef struct { DateSelection *ds; + enum { + TS_STAGE_HOUR, + TS_STAGE_MINUTE, + TS_STAGE_SECOND + } stage; int hour; - bool hour_set; int minute; + bool do_second; + int second; } TimeSelection; typedef enum { @@ -32,12 +38,18 @@ typedef enum { * Initialize the TimeSelection TS. Optionally, get the year, month, and day, * and limits from DS. */ -void time_sel_init(TimeSelection *ts, DateSelection *ds); +void time_sel_init(TimeSelection *ts, DateSelection *ds, bool do_second); /* * Dispatch (process input and draw) the TimeSelection TS according to STATE. * Return: the current state of the selection (back, continue, done). */ -TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state); +TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state, + const char *label); + +/* + * Reset TS back to 00:00. + */ +void time_sel_reset(TimeSelection *ts); #endif diff --git a/src/util.c b/src/util.c index 094cf04..f758ff7 100644 --- a/src/util.c +++ b/src/util.c @@ -88,24 +88,28 @@ static const char *DB_LIMITS_QUERY_STR = "strftime('%e', MinUTC, 'unixepoch') as MinUTCDay,\n" "strftime('%H', RealMin, 'unixepoch') as MinUTCHour,\n" "strftime('%M', RealMin, 'unixepoch') as MinUTCMin,\n" + "strftime('%S', RealMin, 'unixepoch') as MinUTCSec,\n" "MinLocal,\n" "strftime('%Y', MinLocal, 'unixepoch') as MinLocalYear,\n" "strftime('%m', MinLocal, 'unixepoch') as MinLocalMonth,\n" "strftime('%e', MinLocal, 'unixepoch') as MinLocalDay,\n" "strftime('%H', RealMin, 'unixepoch', 'localtime') as MinLocalHour,\n" "strftime('%M', RealMin, 'unixepoch', 'localtime') as MinLocalMin,\n" + "strftime('%S', RealMin, 'unixepoch', 'localtime') as MinLocalSec,\n" "MaxUTC,\n" "strftime('%Y', MaxUTC, 'unixepoch') as MaxUTCYear,\n" "strftime('%m', MaxUTC, 'unixepoch') as MaxUTCMonth,\n" "strftime('%e', MaxUTC, 'unixepoch') as MaxUTCDay,\n" "strftime('%H', RealMax, 'unixepoch') as MaxUTCHour,\n" "strftime('%M', RealMax, 'unixepoch') as MaxUTCMin,\n" + "strftime('%S', RealMax, 'unixepoch') as MaxUTCSec,\n" "MaxLocal,\n" "strftime('%Y', MaxLocal, 'unixepoch') as MaxLocalYear,\n" "strftime('%m', MaxLocal, 'unixepoch') as MaxLocalMonth,\n" "strftime('%e', MaxLocal, 'unixepoch') as MaxLocalDay,\n" "strftime('%H', RealMax, 'unixepoch', 'localtime') as MaxLocalHour,\n" - "strftime('%M', RealMax, 'unixepoch', 'localtime') as MaxLocalMin\n" + "strftime('%M', RealMax, 'unixepoch', 'localtime') as MaxLocalMin,\n" + "strftime('%S', RealMax, 'unixepoch', 'localtime') as MaxLocalSec\n" "FROM\n" "(SELECT\n" "RealMax, RealMin,\n" @@ -115,7 +119,7 @@ static const char *DB_LIMITS_QUERY_STR = "unixepoch(RealMax, 'unixepoch', 'localtime', '+1 ' || ?1, 'start of ' || ?1, '-1 second') as MaxLocal\n" "FROM (SELECT max(time) as RealMax, min(time) as RealMin FROM env_data));"; static sqlite3_stmt *DB_LIMITS_QUERY; -static const char *AVG_FOR_RANGE_QUERY_STR = +static const char *AVG_FOR_PERIOD_QUERY_STR = "SELECT\n" "strftime('%Y', stime, 'unixepoch', 'localtime') as y,\n" "strftime('%m', stime, 'unixepoch', 'localtime') as m,\n" @@ -133,9 +137,9 @@ static const char *AVG_FOR_RANGE_QUERY_STR = "round(avg(temp)) AS atemp,\n" "round(avg(humid)) AS ahumid\n" "FROM env_data\n" - "WHERE time > stime\n" - "AND time < etime);\n"; -static sqlite3_stmt *AVG_FOR_RANGE_QUERY; + "WHERE time >= stime\n" + "AND time <= etime);\n"; +static sqlite3_stmt *AVG_FOR_PERIOD_QUERY; static const char *DATA_POINT_QUERY_STR = "SELECT max(time) IS NULL, max(time), temp, humid FROM env_data WHERE time < ?1\n" "UNION ALL\n" @@ -143,28 +147,38 @@ static const char *DATA_POINT_QUERY_STR = "UNION ALL\n" "SELECT false, * FROM env_data WHERE time == ?1;"; static sqlite3_stmt *DATA_POINT_QUERY; +static const char *AVG_FOR_RANGE_QUERY_STR = + "SELECT count(time), round(avg(temp)), round(avg(humid)) FROM env_data\n" + "WHERE time >= ?1 AND time <= ?2;"; +static sqlite3_stmt *AVG_FOR_RANGE_QUERY; void initialize_util_queries(sqlite3 *db) { int status = sqlite3_prepare_v2(db, DB_LIMITS_QUERY_STR, -1, &DB_LIMITS_QUERY, NULL); if (status != SQLITE_OK) { errx(1, "failed to compile limits query: %s", sqlite3_errstr(status)); } - status = sqlite3_prepare_v2(db, AVG_FOR_RANGE_QUERY_STR, -1, - &AVG_FOR_RANGE_QUERY, NULL); + status = sqlite3_prepare_v2(db, AVG_FOR_PERIOD_QUERY_STR, -1, + &AVG_FOR_PERIOD_QUERY, NULL); if (status != SQLITE_OK) { - errx(1, "failed to compile range average query: %s", sqlite3_errstr(status)); + errx(1, "failed to compile period average query: %s", sqlite3_errstr(status)); } status = sqlite3_prepare_v2(db, DATA_POINT_QUERY_STR, -1, &DATA_POINT_QUERY, NULL); if (status != SQLITE_OK) { errx(1, "failed to compile data point query: %s", sqlite3_errstr(status)); } + status = sqlite3_prepare_v2(db, AVG_FOR_RANGE_QUERY_STR, -1, + &AVG_FOR_RANGE_QUERY, NULL); + if (status != SQLITE_OK) { + errx(1, "failed to compile range average query: %s", sqlite3_errstr(status)); + } } void cleanup_util_queries() { sqlite3_finalize(DB_LIMITS_QUERY); - sqlite3_finalize(AVG_FOR_RANGE_QUERY); + sqlite3_finalize(AVG_FOR_PERIOD_QUERY); sqlite3_finalize(DATA_POINT_QUERY); + sqlite3_finalize(AVG_FOR_RANGE_QUERY); } bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, @@ -184,26 +198,30 @@ bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, start->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 3); start->utc_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 4); start->utc_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 5); - start->local = sqlite3_column_int64(DB_LIMITS_QUERY, 6); - start->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 7); - start->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 8); - start->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 9); - start->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 10); - start->local_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 11); + start->utc_second = sqlite3_column_int64(DB_LIMITS_QUERY, 6); + start->local = sqlite3_column_int64(DB_LIMITS_QUERY, 7); + start->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 8); + start->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 9); + start->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 10); + start->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 11); + start->local_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 12); + start->local_second = sqlite3_column_int64(DB_LIMITS_QUERY, 13); } if (end) { - end->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 12); - end->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 13); - end->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 14); - end->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 15); - end->utc_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 16); - end->utc_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 17); - end->local = sqlite3_column_int64(DB_LIMITS_QUERY, 18); - end->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 19); - end->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 20); - end->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 21); - end->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 22); - end->local_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 23); + end->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 14); + end->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 15); + end->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 16); + end->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 17); + end->utc_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 18); + end->utc_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 19); + end->utc_second = sqlite3_column_int64(DB_LIMITS_QUERY, 20); + end->local = sqlite3_column_int64(DB_LIMITS_QUERY, 21); + end->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 22); + end->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 23); + end->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 24); + end->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 25); + end->local_minute = sqlite3_column_int64(DB_LIMITS_QUERY, 26); + end->local_second = sqlite3_column_int64(DB_LIMITS_QUERY, 27); } } else { success = false; @@ -212,39 +230,37 @@ bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, return success; } -bool get_average_for_range(sqlite3 *db, int year, int month, int day, +bool get_average_for_period(sqlite3 *db, int year, int month, int day, int64_t count, UtilPeriod period, - UtilAverageRange *data) { + UtilAveragePeriod *data) { bool success = true; - sqlite3_bind_int(AVG_FOR_RANGE_QUERY, 1, year); - sqlite3_bind_int(AVG_FOR_RANGE_QUERY, 2, month); - sqlite3_bind_int(AVG_FOR_RANGE_QUERY, 3, day); - sqlite3_bind_int64(AVG_FOR_RANGE_QUERY, 4, count); + sqlite3_bind_int(AVG_FOR_PERIOD_QUERY, 1, year); + sqlite3_bind_int(AVG_FOR_PERIOD_QUERY, 2, month); + sqlite3_bind_int(AVG_FOR_PERIOD_QUERY, 3, day); + sqlite3_bind_int64(AVG_FOR_PERIOD_QUERY, 4, count); if (period == PERIOD_WEEK) { - sqlite3_bind_text(AVG_FOR_RANGE_QUERY, 5, "days", -1, SQLITE_STATIC); - sqlite3_bind_int(AVG_FOR_RANGE_QUERY, 6, 7); + sqlite3_bind_text(AVG_FOR_PERIOD_QUERY, 5, "days", -1, SQLITE_STATIC); + sqlite3_bind_int(AVG_FOR_PERIOD_QUERY, 6, 7); } else { - sqlite3_bind_text(AVG_FOR_RANGE_QUERY, 5, PERIOD_LABELS[period], -1, + sqlite3_bind_text(AVG_FOR_PERIOD_QUERY, 5, PERIOD_LABELS[period], -1, SQLITE_STATIC); - sqlite3_bind_int(AVG_FOR_RANGE_QUERY, 6, 1); + sqlite3_bind_int(AVG_FOR_PERIOD_QUERY, 6, 1); } - int status = sqlite3_step(AVG_FOR_RANGE_QUERY); - if (status == SQLITE_ROW) { - if (data) { - data->year = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 0); - data->month = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 1); - data->day = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 2); - data->hour = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 3); - data->npoints = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 4); - data->upper_bound = sqlite3_column_int64(AVG_FOR_RANGE_QUERY, 5); - data->lower_bound = sqlite3_column_int64(AVG_FOR_RANGE_QUERY, 6); - data->temp = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 7); - data->humid = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 8); - } - } else { + int status = sqlite3_step(AVG_FOR_PERIOD_QUERY); + if (status != SQLITE_ROW) { success = false; + } else if (data) { + data->year = sqlite3_column_int(AVG_FOR_PERIOD_QUERY, 0); + data->month = sqlite3_column_int(AVG_FOR_PERIOD_QUERY, 1); + data->day = sqlite3_column_int(AVG_FOR_PERIOD_QUERY, 2); + data->hour = sqlite3_column_int(AVG_FOR_PERIOD_QUERY, 3); + data->npoints = sqlite3_column_int64(AVG_FOR_PERIOD_QUERY, 4); + data->upper_bound = sqlite3_column_int64(AVG_FOR_PERIOD_QUERY, 5); + data->lower_bound = sqlite3_column_int64(AVG_FOR_PERIOD_QUERY, 6); + data->temp = sqlite3_column_int(AVG_FOR_PERIOD_QUERY, 7); + data->humid = sqlite3_column_int(AVG_FOR_PERIOD_QUERY, 8); } - sqlite3_reset(AVG_FOR_RANGE_QUERY); + sqlite3_reset(AVG_FOR_PERIOD_QUERY); return success; } @@ -299,3 +315,19 @@ bool get_data_point_info(sqlite3 *db, int64_t time, UtilDataPointInfo *info) { return false; } } + +bool get_average_for_range(sqlite3 *db, uint64_t start, uint64_t end, + UtilAverageRange *data) { + bool success = true; + sqlite3_bind_int64(AVG_FOR_RANGE_QUERY, 1, start); + sqlite3_bind_int64(AVG_FOR_RANGE_QUERY, 2, end); + if (sqlite3_step(AVG_FOR_RANGE_QUERY) != SQLITE_ROW) { + success = false; + } else if (data) { + data->npoints = sqlite3_column_int64(AVG_FOR_RANGE_QUERY, 0); + data->temp = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 1); + data->humid = sqlite3_column_int(AVG_FOR_RANGE_QUERY, 2); + } + sqlite3_reset(AVG_FOR_RANGE_QUERY); + return success; +} diff --git a/src/util.h b/src/util.h index 1791a88..bf8d38f 100644 --- a/src/util.h +++ b/src/util.h @@ -99,12 +99,14 @@ typedef struct { int utc_day; int utc_hour; int utc_minute; + int utc_second; int64_t local; int local_year; int local_month; int local_day; int local_hour; int local_minute; + int local_second; } UtilDate; enum { @@ -120,14 +122,14 @@ extern const size_t NPERIOD; /* * Return the START of the first and END of the last PERIOD (ex. week) of DB. - * Also get the hour and minute limits for the first and last day. + * Also get the hour, minute, and second limits for the first and last day. * Return: false if an error occurred, true otherwise. */ bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, UtilDate *end); typedef struct { - int npoints; + int64_t npoints; int temp; int humid; int year; @@ -136,16 +138,16 @@ typedef struct { int hour; bool lower_bound; bool upper_bound; -} UtilAverageRange; +} UtilAveragePeriod; /* * Get the average temp. and humid. between YEAR-MONTH-DAY + COUNT * PERIOD and * YEAR-MONTH-DAY + (COUNT + 1) * PERIOD. * Return: false if an error occurred, true otherwise. */ -bool get_average_for_range(sqlite3 *db, int year, int month, int day, +bool get_average_for_period(sqlite3 *db, int year, int month, int day, int64_t count, UtilPeriod period, - UtilAverageRange *data); + UtilAveragePeriod *data); typedef struct { int64_t time; @@ -168,4 +170,17 @@ typedef struct { */ bool get_data_point_info(sqlite3 *db, int64_t time, UtilDataPointInfo *info); +typedef struct { + int64_t npoints; + int temp; + int humid; +} UtilAverageRange; + +/* + * Get the average DATA for the range of UTC times START to END (inclusive). + * Return: false if an error occurred, true otherwise. + */ +bool get_average_for_range(sqlite3 *db, uint64_t start, uint64_t end, + UtilAverageRange *data); + #endif