diff --git a/Makefile b/Makefile index 8251041..f574b9b 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,9 @@ CC=clang CFLAGS=-g -std=c11 -Wall ${SQLITE3_CFLAGS} LD=clang -LDFLAGS=-lgpio -lfigpar -lgeom -lpthread ${SQLITE3_LDFLAGS} +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/datesel.c src/ui/statsby.c src/ui/datapoints.c src/ui/timesel.c PROG=rpi4b-temp-humidity OBJS=${SRCS:C/^src/bin/:C/.c$/.o/} @@ -15,15 +15,24 @@ 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/main.o bin/lcd.o bin/screen.o bin/ui/datesel.o: src/lcd.h -bin/ui/statsby.o: src/lcd.h +bin/ui/statsby.o bin/ui/timesel.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/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/main.o bin/ths.o bin/ui/screen.o bin/ui/statsby.o: src/ths.h +bin/ui/timesel.o: src/ths.h + +bin/main.o bin/ui/timesel.o bin/ui/datapoints.o: src/ui/timesel.h bin/main.o bin/button.o bin/ui/screen.o: src/button.h -bin/main.o bin/ui/screen.o bin/ui/statsby.o bin/ui/datesel.o: src/ui/screen.h -bin/main.o bin/ui/datesel.o bin/ui/statsby.o: src/ui/datesel.h bin/main.o bin/ui/statsby.o: src/ui/statsby.h +bin/main.o bin/ui/datapoints.o: src/ui/datapoints.h ${OBJS}: ${.TARGET:C/^bin/src/:C/.o$/.c/} @mkdir -p ${.TARGET:H} diff --git a/src/main.c b/src/main.c index 78d130f..0a3a178 100644 --- a/src/main.c +++ b/src/main.c @@ -14,6 +14,7 @@ #include "menu.h" #include "ui/screen.h" #include "ui/statsby.h" +#include "ui/datapoints.h" #include #include @@ -103,6 +104,7 @@ int main(int argc, char *const *argv) { GLOBAL_OPTS.sel_pin); 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()); while (RUNNING) { lock_stat_globals(); uint32_t temp = LAST_TEMP; diff --git a/src/ui/datapoints.c b/src/ui/datapoints.c new file mode 100644 index 0000000..486fba6 --- /dev/null +++ b/src/ui/datapoints.c @@ -0,0 +1,164 @@ +/* + * datapoints.c - Screen for viewing individual data points + * 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 "datapoints.h" +#include "../ths.h" + +#include +#include +#include + +static bool data_points_select_date(DataPointsScreen *screen, + SensorState *state) { + if (state->up_down || state->down_down || state->sel_down || state->back_down) { + screen->need_redraw = true; + } + if (screen->need_redraw) { + screen->need_redraw = false; + DateSelectionState dss = date_sel_dispatch(&screen->ds, state, + PERIOD_DAY, "Date:"); + switch (dss) { + 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 data_points_calculate_initial_time(DataPointsScreen *screen) { + struct tm broken_time = { + .tm_sec = 0, + .tm_min = screen->ts.minute, + .tm_hour = screen->ts.hour, + .tm_mday = screen->ds.day, + .tm_mon = screen->ds.month - 1, + .tm_year = screen->ds.year - 1900, + .tm_isdst = -1, + }; + time_t it = mktime(&broken_time); + if (it == -1) { + warnx("could not convert selected date and time"); + screen->cur_point = 0; + } else { + screen->cur_point = it; + } +} + +static void data_points_select_time(DataPointsScreen *screen, + SensorState *state) { + if (state->up_down || state->down_down || state->sel_down || state->back_down) { + screen->need_redraw = true; + } + if (screen->need_redraw) { + screen->need_redraw = false; + TimeSelState tss = time_sel_dispatch(&screen->ts, state); + switch (tss) { + case TIME_SEL_CONTINUE: + break; // ignore + case TIME_SEL_DONE: + ++screen->stage; + screen->need_redraw = true; + data_points_calculate_initial_time(screen); + break; + case TIME_SEL_BACK: + case TIME_SEL_ERROR: + screen->need_redraw = true; + --screen->stage; + } + } +} + +static void data_points_show(DataPointsScreen *screen, SensorState *state) { + if (state->back_down) { + screen->stage = DP_STAGE_DATE; + screen->need_redraw = true; + return; + } + if (state->up_down || state->down_down) { + screen->need_redraw = true; + } + if (screen->need_redraw) { + screen->need_redraw = false; + UtilDataPointInfo info; + if (!get_data_point_info(state->db, screen->cur_point, &info)) { + warnx("could not get info for data point: %" PRIi64 "", screen->cur_point); + screen->stage = DP_STAGE_DATE; + screen->need_redraw = true; + return; + } + screen->cur_point = info.time; + int temp = info.temp; + int humid = info.humid; + if (state->up_down && info.has_next) { + screen->cur_point = info.next_time; + temp = info.next_temp; + humid = info.next_humid; + } else if (state->down_down && info.has_prev) { + screen->cur_point = info.prev_time; + temp = info.prev_temp; + humid = info.prev_humid; + } + struct tm broken_time; + localtime_r(&screen->cur_point, &broken_time); + char str_time[17]; + if (!strftime(str_time, 17, "%y/%m/%d %H:%M", &broken_time)) { + warnx("could not format time: %" PRIi64 "", screen->cur_point); + screen->stage = DP_STAGE_DATE; + screen->need_redraw = true; + return; + } + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, str_time); + char data_line[17]; + snprintf(data_line, 17, "%02ds %.1fF %d%%", broken_time.tm_sec, + DK_TO_F(temp), humid); + lcd_move_to(state->lcd, 1, 0); + lcd_write_string(state->lcd, data_line); + } +} + +static bool data_points_screen_dispatch(DataPointsScreen *screen, SensorState *state) { + if (state->force_draw) { + screen->need_redraw = true; + } + switch (screen->stage) { + case DP_STAGE_DATE: + return data_points_select_date(screen, state); + case DP_STAGE_TIME: + data_points_select_time(screen, state); + return false; + case DP_STAGE_SHOW: + data_points_show(screen, state); + return false; + default: + warnx("Attempt to show bad data points screen stage"); + return true; + } +} + +DataPointsScreen *data_points_screen_new() { + DataPointsScreen *s = malloc_checked(sizeof(DataPointsScreen)); + screen_init(&s->parent, "Data Points", + (ScreenDispatchFunc) data_points_screen_dispatch, + (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); + return s; +} diff --git a/src/ui/datapoints.h b/src/ui/datapoints.h new file mode 100644 index 0000000..97dccf0 --- /dev/null +++ b/src/ui/datapoints.h @@ -0,0 +1,35 @@ +/* + * datapoints.h - Screen for viewing individual data points + * 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_DATAPOINTS_H +#define INCLUDED_DATAPOINTS_H + +#include "datesel.h" +#include "timesel.h" + +typedef struct { + Screen parent; + bool need_redraw; + enum { + DP_STAGE_DATE = 0, + DP_STAGE_TIME, + DP_STAGE_SHOW, + } stage; + DateSelection ds; + TimeSelection ts; + int64_t cur_point; +} DataPointsScreen; + +/* + * Create a new data points screen. This screen will display individual + * temperature and humidity data points. + */ +DataPointsScreen *data_points_screen_new(void); + +#endif diff --git a/src/ui/datesel.c b/src/ui/datesel.c index 574934d..6e6f9c1 100644 --- a/src/ui/datesel.c +++ b/src/ui/datesel.c @@ -1,5 +1,5 @@ /* - * datesel.h - Date selector + * datesel.c - Date selector * Copyright (C) 2024 Alexander Rosenberg * * This program is free software: you can redistribute it and/or modify it under @@ -10,12 +10,10 @@ #include "datesel.h" #include +#include -void date_sel_init(DateSelection *ds, int start_line, int start_col, - DateSelectionStage max_stage) { +void date_sel_init(DateSelection *ds, DateSelectionStage max_stage) { ds->max_stage = max_stage; - ds->start_line = start_line; - ds->start_col = start_col; memset(&ds->start_time, 0, sizeof(UtilDate)); // min date memset(&ds->end_time, 0xff, sizeof(UtilDate)); // max date date_sel_reset(ds); @@ -99,7 +97,23 @@ static void date_sel_add_units(DateSelection *ds, int n) { date_sel_cleanup(ds); } -DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { +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)) { + warnx("failed to query database limits"); + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + return DATE_SEL_ERROR; + } + ds->start_time = start; + ds->end_time = end; + lcd_clear(state->lcd); + lcd_move_to(state->lcd, 0, 0); + lcd_write_string(state->lcd, label); + lcd_move_to(state->lcd, 1, 0); if (ds->year == -1) { time_t utc_time = time(NULL); struct tm local_time; @@ -121,6 +135,8 @@ DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { } if (state->back_down) { if (ds->stage == DATE_SEL_YEAR) { + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); return DATE_SEL_BACK; } --ds->stage; @@ -130,11 +146,13 @@ DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { } if (state->sel_down) { if (ds->stage == ds->max_stage) { + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); return DATE_SEL_DONE; } ++ds->stage; } - int buf_size = 17 - ds->start_col; + int buf_size = 17; char buff[buf_size]; int cursor_pos; const char *format; @@ -153,14 +171,15 @@ DateSelectionState date_sel_dispatch(DateSelection *ds, SensorState *state) { break; default: LOG_VERBOSE("Date selector tried to select bad field\n"); + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); return DATE_SEL_BACK; } snprintf(buff, buf_size, format, ds->year, ds->month, ds->day); - cursor_pos += ds->start_col; - lcd_move_to(state->lcd, ds->start_line, ds->start_col); + lcd_move_to(state->lcd, 1, 0); lcd_write_string(state->lcd, buff); lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, LCD_DISPLAY_ON); - lcd_move_to(state->lcd, ds->start_line, cursor_pos); + lcd_move_to(state->lcd, 1, cursor_pos); return DATE_SEL_CONTINUE; } diff --git a/src/ui/datesel.h b/src/ui/datesel.h index c422a2e..23e5b91 100644 --- a/src/ui/datesel.h +++ b/src/ui/datesel.h @@ -25,8 +25,6 @@ typedef struct { int day; DateSelectionStage stage; DateSelectionStage max_stage; - int start_line; - int start_col; UtilDate start_time; UtilDate end_time; } DateSelection; @@ -34,16 +32,15 @@ typedef struct { typedef enum { DATE_SEL_BACK, DATE_SEL_CONTINUE, - DATE_SEL_DONE + DATE_SEL_DONE, + DATE_SEL_ERROR } DateSelectionState; /* - * Initialize a DateSelection. START_LINE and START_COL are the first line and - * column where it should draw the widget. MAX_STAGE is the most specific field + * Initialize a DateSelection. MAX_STAGE is the most specific field * to select. */ -void date_sel_init(DateSelection *ds, int start_line, int start_col, - DateSelectionStage max_stage); +void date_sel_init(DateSelection *ds, DateSelectionStage max_stage); /* * Reset the date on DS. @@ -54,6 +51,9 @@ void date_sel_reset(DateSelection *ds); * 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); +DateSelectionState date_sel_dispatch(DateSelection *ds, + SensorState *state, + UtilPeriod limit_period, + const char *label); #endif diff --git a/src/ui/statsby.c b/src/ui/statsby.c index fc38d92..ca579d5 100644 --- a/src/ui/statsby.c +++ b/src/ui/statsby.c @@ -1,5 +1,5 @@ /* - * statsby.h - Screen for view stats by a specified period (day, week, etc.) + * statsby.c - Screen for view stats by a specified period (day, week, etc.) * Copyright (C) 2024 Alexander Rosenberg * * This program is free software: you can redistribute it and/or modify it under @@ -59,36 +59,24 @@ static void stats_by_select_start(StatsByScreen *screen, SensorState *state) { screen->need_redraw = true; } if (screen->need_redraw) { - UtilDate start, end; - if (!get_database_limits(state->db, PERIOD_LABELS[screen->period], - &start, &end)) { - warnx("failed to query database limits"); - --screen->stage; - return; - } - screen->ds.start_time = start; - screen->ds.end_time = end; - lcd_clear(state->lcd); - lcd_move_to(state->lcd, 0, 0); - lcd_write_string(state->lcd, "Start: "); - lcd_move_to(state->lcd, 1, 0); screen->need_redraw = false; - DateSelectionState dss = date_sel_dispatch(&screen->ds, state); + DateSelectionState dss = date_sel_dispatch(&screen->ds, state, + screen->period, "Start:"); switch (dss) { - case DATE_SEL_BACK: - screen->need_redraw = true; - lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, - LCD_DISPLAY_ON); - --screen->stage; - return; case DATE_SEL_CONTINUE: break; // ignore case DATE_SEL_DONE: ++screen->stage; screen->offset_scale = 0; screen->need_redraw = true; + screen->blimit_reached = false; screen->ulimit_reached = false; break; + case DATE_SEL_BACK: + case DATE_SEL_ERROR: + --screen->stage; + screen->need_redraw = true; + break; } } } @@ -200,6 +188,8 @@ StatsByScreen *stats_by_screen_new() { (ScreenDispatchFunc) stats_by_screen_dispatch, (ScreenCleanupFunc) free); s->need_redraw = true; - date_sel_init(&s->ds, 1, 0, DATE_SEL_DAY); + s->stage = STATS_BY_SELECT_PERIOD; + s->period = PERIOD_HOUR; + date_sel_init(&s->ds, DATE_SEL_DAY); return s; } diff --git a/src/ui/timesel.c b/src/ui/timesel.c new file mode 100644 index 0000000..6e0aec1 --- /dev/null +++ b/src/ui/timesel.c @@ -0,0 +1,118 @@ +/* + * timesel.c - Time selector + * 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 "timesel.h" + +#include + +void time_sel_init(TimeSelection *ts, DateSelection *ds) { + ts->hour = 0; + ts->minute = 0; + ts->hour_set = false; + ts->ds = ds; +} + +static void time_sel_clamp(TimeSelection *ts) { + if (ts->ds) { + if (ts->ds->end_time.local_year == ts->ds->year && + ts->ds->end_time.local_month == ts->ds->month && + ts->ds->end_time.local_day == ts->ds->day) { + if (ts->hour > ts->ds->end_time.local_hour) { + ts->hour = ts->ds->end_time.local_hour; + } + if (ts->hour == ts->ds->end_time.local_hour && + ts->minute > ts->ds->end_time.local_minute) { + ts->minute = ts->ds->end_time.local_minute; + } + } 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) { + if (ts->hour < ts->ds->start_time.local_hour) { + ts->hour = ts->ds->start_time.local_hour; + } + if (ts->hour == ts->ds->start_time.local_hour && + ts->minute < ts->ds->start_time.local_minute) { + ts->minute = ts->ds->start_time.local_minute; + } + } + } +} + +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; + } + if (ts->hour < 0) { + ts->hour = 24 + ts->hour; + } + if (ts->minute < 0) { + ts->minute = 60 + ts->minute; + } + time_sel_clamp(ts); +} + +TimeSelState time_sel_dispatch(TimeSelection *ts, SensorState *state) { + if (state->back_down) { + if (ts->hour_set) { + ts->hour_set = false; + } else { + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + return TIME_SEL_BACK; + } + } + if (state->sel_down) { + if (!ts->hour_set) { + ts->hour_set = true; + } else { + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + return TIME_SEL_DONE; + } + } + if (ts->ds) { + UtilDate start, end; + if (!get_database_limits(state->db, PERIOD_DAY, &start, &end)) { + warnx("failed to query database limits"); + lcd_display_control(state->lcd, LCD_CURSOR_NO_BLINK, LCD_CURSOR_OFF, + LCD_DISPLAY_ON); + return TIME_SEL_ERROR; + } + ts->ds->start_time = start; + ts->ds->end_time = end; + } + 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; + } + char time_line[17]; + 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); + lcd_display_control(state->lcd, LCD_CURSOR_BLINK, LCD_CURSOR_ON, + LCD_DISPLAY_ON); + return TIME_SEL_CONTINUE; +} diff --git a/src/ui/timesel.h b/src/ui/timesel.h new file mode 100644 index 0000000..a045c3b --- /dev/null +++ b/src/ui/timesel.h @@ -0,0 +1,43 @@ +/* + * timesel.h - Time selector + * 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_TIMESEL_H +#define INCLUDED_TIMESEL_H + +#include "../util.h" +#include "screen.h" +#include "datesel.h" + +typedef struct { + DateSelection *ds; + int hour; + bool hour_set; + int minute; +} TimeSelection; + +typedef enum { + TIME_SEL_BACK, + TIME_SEL_CONTINUE, + TIME_SEL_DONE, + TIME_SEL_ERROR, +} TimeSelState; + +/* + * Initialize the TimeSelection TS. Optionally, get the year, month, and day, + * and limits from DS. + */ +void time_sel_init(TimeSelection *ts, DateSelection *ds); + +/* + * 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); + +#endif diff --git a/src/util.c b/src/util.c index 722d5d1..094cf04 100644 --- a/src/util.c +++ b/src/util.c @@ -11,6 +11,7 @@ #include #include +#include const char *PERIOD_LABELS[] = { "HOUR", @@ -86,21 +87,25 @@ static const char *DB_LIMITS_QUERY_STR = "strftime('%m', MinUTC, 'unixepoch') as MinUTCMonth,\n" "strftime('%e', MinUTC, 'unixepoch') as MinUTCDay,\n" "strftime('%H', RealMin, 'unixepoch') as MinUTCHour,\n" + "strftime('%M', RealMin, 'unixepoch') as MinUTCMin,\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" "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" "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('%H', RealMax, 'unixepoch', 'localtime') as MaxLocalHour,\n" + "strftime('%M', RealMax, 'unixepoch', 'localtime') as MaxLocalMin\n" "FROM\n" "(SELECT\n" "RealMax, RealMin,\n" @@ -110,7 +115,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; -const char *AVG_FOR_RANGE_QUERY_STR = +static const char *AVG_FOR_RANGE_QUERY_STR = "SELECT\n" "strftime('%Y', stime, 'unixepoch', 'localtime') as y,\n" "strftime('%m', stime, 'unixepoch', 'localtime') as m,\n" @@ -131,6 +136,13 @@ const char *AVG_FOR_RANGE_QUERY_STR = "WHERE time > stime\n" "AND time < etime);\n"; static sqlite3_stmt *AVG_FOR_RANGE_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" + "SELECT min(time) IS NULL, min(time), temp, humid FROM env_data WHERE time > ?1\n" + "UNION ALL\n" + "SELECT false, * FROM env_data WHERE time == ?1;"; +static sqlite3_stmt *DATA_POINT_QUERY; void initialize_util_queries(sqlite3 *db) { int status = sqlite3_prepare_v2(db, DB_LIMITS_QUERY_STR, -1, &DB_LIMITS_QUERY, NULL); @@ -142,21 +154,27 @@ void initialize_util_queries(sqlite3 *db) { if (status != SQLITE_OK) { errx(1, "failed to compile range 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)); + } } void cleanup_util_queries() { sqlite3_finalize(DB_LIMITS_QUERY); sqlite3_finalize(AVG_FOR_RANGE_QUERY); + sqlite3_finalize(DATA_POINT_QUERY); } - -bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start, +bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, UtilDate *end) { - if (strcasecmp(period, "week") == 0 || strcasecmp(period, "hour") == 0) { - period = "day"; + if (period == PERIOD_WEEK || period == PERIOD_HOUR) { + period = PERIOD_DAY; } bool success = true; - sqlite3_bind_text(DB_LIMITS_QUERY, 1, period, -1, SQLITE_STATIC); + sqlite3_bind_text(DB_LIMITS_QUERY, 1, PERIOD_LABELS[period], -1, + SQLITE_STATIC); int status = sqlite3_step(DB_LIMITS_QUERY); if (status == SQLITE_ROW) { if (start) { @@ -165,29 +183,31 @@ bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start, start->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 2); start->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 3); start->utc_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 4); - start->local = sqlite3_column_int64(DB_LIMITS_QUERY, 5); - start->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 6); - start->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 7); - start->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 8); - start->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 9); + 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); } if (end) { - end->utc = sqlite3_column_int64(DB_LIMITS_QUERY, 10); - end->utc_year = sqlite3_column_int64(DB_LIMITS_QUERY, 11); - end->utc_month = sqlite3_column_int64(DB_LIMITS_QUERY, 12); - end->utc_day = sqlite3_column_int64(DB_LIMITS_QUERY, 13); - end->utc_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 14); - end->local = sqlite3_column_int64(DB_LIMITS_QUERY, 15); - end->local_year = sqlite3_column_int64(DB_LIMITS_QUERY, 16); - end->local_month = sqlite3_column_int64(DB_LIMITS_QUERY, 17); - end->local_day = sqlite3_column_int64(DB_LIMITS_QUERY, 18); - end->local_hour = sqlite3_column_int64(DB_LIMITS_QUERY, 19); + 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); } } else { success = false; } - // unbind the string so it can be freed by the caller - sqlite3_bind_null(DB_LIMITS_QUERY, 1); sqlite3_reset(DB_LIMITS_QUERY); return success; } @@ -227,3 +247,55 @@ bool get_average_for_range(sqlite3 *db, int year, int month, int day, sqlite3_reset(AVG_FOR_RANGE_QUERY); return success; } + +bool get_data_point_info(sqlite3 *db, int64_t time, UtilDataPointInfo *info) { + sqlite3_bind_int64(DATA_POINT_QUERY, 1, time); + if (sqlite3_step(DATA_POINT_QUERY) != SQLITE_ROW) { + sqlite3_reset(DATA_POINT_QUERY); + return false; + } + info->has_prev = !sqlite3_column_int(DATA_POINT_QUERY, 0); + info->prev_time = sqlite3_column_int64(DATA_POINT_QUERY, 1); + info->prev_temp = sqlite3_column_int64(DATA_POINT_QUERY, 2); + info->prev_humid = sqlite3_column_int64(DATA_POINT_QUERY, 3); + if (sqlite3_step(DATA_POINT_QUERY) != SQLITE_ROW) { + sqlite3_reset(DATA_POINT_QUERY); + return false; + } + info->has_next = !sqlite3_column_int(DATA_POINT_QUERY, 0); + info->next_time = sqlite3_column_int64(DATA_POINT_QUERY, 1); + info->next_temp = sqlite3_column_int64(DATA_POINT_QUERY, 2); + info->next_humid = sqlite3_column_int64(DATA_POINT_QUERY, 3); + int status = sqlite3_step(DATA_POINT_QUERY); + if (status == SQLITE_DONE) { + // we need to select the closes data point + int64_t closest_time; + if (!info->has_next && !info->has_prev) { + // no data points + return false; + } else if (info->has_next && !info->has_prev) { + closest_time = info->next_time; + } else if (info->has_prev && !info->has_next) { + closest_time = info->prev_time; + } else { + int64_t next_dist = labs(time - info->next_time); + int64_t prev_dist = labs(time - info->prev_time); + if (next_dist < prev_dist) { + closest_time = info->next_time; + } else { + closest_time = info->prev_time; + } + } + sqlite3_reset(DATA_POINT_QUERY); + return get_data_point_info(db, closest_time, info); + } else if (status == SQLITE_ROW) { + info->time = sqlite3_column_int64(DATA_POINT_QUERY, 1); + info->temp = sqlite3_column_int64(DATA_POINT_QUERY, 2); + info->humid = sqlite3_column_int64(DATA_POINT_QUERY, 3); + sqlite3_reset(DATA_POINT_QUERY); + return true; + } else { + sqlite3_reset(DATA_POINT_QUERY); + return false; + } +} diff --git a/src/util.h b/src/util.h index e4fb646..1791a88 100644 --- a/src/util.h +++ b/src/util.h @@ -98,11 +98,13 @@ typedef struct { int utc_month; int utc_day; int utc_hour; + int utc_minute; int64_t local; int local_year; int local_month; int local_day; int local_hour; + int local_minute; } UtilDate; enum { @@ -118,9 +120,10 @@ 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. * Return: false if an error occurred, true otherwise. */ -bool get_database_limits(sqlite3 *db, const char *period, UtilDate *start, +bool get_database_limits(sqlite3 *db, UtilPeriod period, UtilDate *start, UtilDate *end); typedef struct { @@ -144,4 +147,25 @@ bool get_average_for_range(sqlite3 *db, int year, int month, int day, int64_t count, UtilPeriod period, UtilAverageRange *data); +typedef struct { + int64_t time; + int temp; + int humid; + bool has_next; + int64_t next_time; + int next_temp; + int next_humid; + bool has_prev; + int64_t prev_time; + int prev_temp; + int prev_humid; +} UtilDataPointInfo; + +/* + * Get the data, next point, and previous point for data point TIME. INFO must + * not be NULL. + * Return: false if an error occurred, true otherwise. + */ +bool get_data_point_info(sqlite3 *db, int64_t time, UtilDataPointInfo *info); + #endif