/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Copyright (C) 2008 Richard Hughes * * Licensed under the GNU General Public License Version 2 * * 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 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "config.h" #include #include #include #include #include #include "up-history.h" #include "up-stats-item.h" #include "up-history-item.h" static void up_history_finalize (GObject *object); #define UP_HISTORY_SAVE_INTERVAL (10*60) /* seconds */ #define UP_HISTORY_SAVE_INTERVAL_LOW_POWER 5 /* seconds */ #define UP_HISTORY_LOW_POWER_PERCENT 10 #define UP_HISTORY_DEFAULT_MAX_DATA_AGE (7*24*60*60) /* seconds */ struct UpHistoryPrivate { gchar *id; gdouble rate_last; gint64 time_full_last; gint64 time_empty_last; gdouble percentage_last; UpDeviceState state; GPtrArray *data_rate; GPtrArray *data_charge; GPtrArray *data_time_full; GPtrArray *data_time_empty; GSource *save_source; guint max_data_age; gchar *dir; }; enum { UP_HISTORY_PROGRESS, UP_HISTORY_LAST_SIGNAL }; G_DEFINE_TYPE_WITH_PRIVATE (UpHistory, up_history, G_TYPE_OBJECT) /** * up_history_set_max_data_age: **/ void up_history_set_max_data_age (UpHistory *history, guint max_data_age) { history->priv->max_data_age = max_data_age; } /** * up_history_array_copy_cb: **/ static void up_history_array_copy_cb (UpHistoryItem *item, GPtrArray *dest) { g_ptr_array_add (dest, g_object_ref (item)); } /** * up_history_array_limit_resolution: * @array: The data we have for a specific graph * @max_num: The max desired points * * We need to reduce the number of data points else the graph will take a long * time to plot accuracy we don't need at the larger scales. * This will not reduce the scale or range of the data. * * 100 + + | + | + | + + * | A | | | * 80 + + | + | + | + + * | | B C | | * 60 + + | + | + | + + * | | | | * 40 + + | + | + | + E + * | | | | D * 20 + + | + | + | + F + * | | | | * 0 +-----+-----+-----+-----+-----+ * 20 40 60 80 100 * * A = 15,90 * B = 30,70 * C = 52,70 * D = 80,30 * E = 85,40 * F = 90,20 * * 1 = 15,90 * 2 = 41,70 * 3 = 85,30 **/ static GPtrArray * up_history_array_limit_resolution (GPtrArray *array, guint max_num) { UpHistoryItem *item; UpHistoryItem *item_new; guint length; guint i; guint64 last; guint64 first; GPtrArray *new; UpDeviceState state = UP_DEVICE_STATE_UNKNOWN; guint64 time_s = 0; gdouble value = 0; guint64 count = 0; guint step = 1; new = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); g_debug ("length of array (before) %i", array->len); /* check length */ length = array->len; if (length == 0) goto out; if (length < max_num) { /* need to copy array */ g_ptr_array_foreach (array, (GFunc) up_history_array_copy_cb, new); goto out; } /* last element */ item = (UpHistoryItem *) g_ptr_array_index (array, length-1); last = up_history_item_get_time (item); item = (UpHistoryItem *) g_ptr_array_index (array, 0); first = up_history_item_get_time (item); /* Reduces the number of points to a pre-set level using a time * division algorithm so we don't keep diluting the previous * data with a conventional 1-in-x type algorithm. */ for (i = 0; i < length; i++) { guint64 preset; item = (UpHistoryItem *) g_ptr_array_index (array, i); preset = last + ((first - last) * (guint64) step) / max_num; /* if state changed or we went over the preset do a new point */ if (count > 0 && (up_history_item_get_time (item) > preset || up_history_item_get_state (item) != state)) { item_new = up_history_item_new (); up_history_item_set_time (item_new, time_s / count); up_history_item_set_value (item_new, value / count); up_history_item_set_state (item_new, state); g_ptr_array_add (new, item_new); step++; time_s = up_history_item_get_time (item); value = up_history_item_get_value (item); state = up_history_item_get_state (item); count = 1; } else { count++; time_s += up_history_item_get_time (item); value += up_history_item_get_value (item); } } /* only add if nonzero */ if (count > 0) { item_new = up_history_item_new (); up_history_item_set_time (item_new, time_s / count); up_history_item_set_value (item_new, value / count); up_history_item_set_state (item_new, state); g_ptr_array_add (new, item_new); } /* check length */ g_debug ("length of array (after) %i", new->len); out: return new; } /** * up_history_copy_array_timespan: **/ static GPtrArray * up_history_copy_array_timespan (const GPtrArray *array, guint timespan) { guint i; UpHistoryItem *item; GPtrArray *array_new; GTimeVal timeval; /* no data */ if (array->len == 0) return NULL; /* no limit on data */ if (timespan == 0) { array_new = g_ptr_array_ref ((GPtrArray *) array); goto out; } /* new data */ array_new = g_ptr_array_new (); g_get_current_time (&timeval); g_debug ("limiting data to last %i seconds", timespan); /* treat the timespan like a range, and search backwards */ timespan *= 0.95f; for (i=array->len-1; i>0; i--) { item = (UpHistoryItem *) g_ptr_array_index (array, i); if (timeval.tv_sec - up_history_item_get_time (item) < timespan) g_ptr_array_add (array_new, g_object_ref (item)); } out: return array_new; } /** * up_history_get_data: **/ GPtrArray * up_history_get_data (UpHistory *history, UpHistoryType type, guint timespan, guint resolution) { GPtrArray *array; GPtrArray *array_resolution; const GPtrArray *array_data = NULL; g_return_val_if_fail (UP_IS_HISTORY (history), NULL); if (history->priv->id == NULL) return NULL; if (type == UP_HISTORY_TYPE_CHARGE) array_data = history->priv->data_charge; else if (type == UP_HISTORY_TYPE_RATE) array_data = history->priv->data_rate; else if (type == UP_HISTORY_TYPE_TIME_FULL) array_data = history->priv->data_time_full; else if (type == UP_HISTORY_TYPE_TIME_EMPTY) array_data = history->priv->data_time_empty; /* not recognised */ if (array_data == NULL) return NULL; /* only return a certain time */ array = up_history_copy_array_timespan (array_data, timespan); if (array == NULL) return NULL; /* only add a certain number of points */ array_resolution = up_history_array_limit_resolution (array, resolution); g_ptr_array_unref (array); return array_resolution; } /** * up_history_get_profile_data: **/ GPtrArray * up_history_get_profile_data (UpHistory *history, gboolean charging) { guint i; guint non_zero_accuracy = 0; gfloat average = 0.0f; guint bin; guint oldbin = 999; UpHistoryItem *item_last = NULL; UpHistoryItem *item; UpHistoryItem *item_old = NULL; UpStatsItem *stats; GPtrArray *array; GPtrArray *data; guint time_s; gdouble value; gdouble total_value = 0.0f; g_return_val_if_fail (UP_IS_HISTORY (history), NULL); /* create 100 item list and set to zero */ data = g_ptr_array_new_full (101, g_object_unref); for (i=0; i<101; i++) { stats = up_stats_item_new (); g_ptr_array_add (data, stats); } array = history->priv->data_charge; for (i=0; ilen; i++) { item = (UpHistoryItem *) g_ptr_array_index (array, i); if (item_last == NULL || up_history_item_get_state (item) != up_history_item_get_state (item_last)) { item_old = NULL; goto cont; } /* round to the nearest int */ bin = rint (up_history_item_get_value (item)); /* ensure bin is in range */ if (bin >= data->len) bin = data->len - 1; /* different */ if (oldbin != bin) { oldbin = bin; if (item_old != NULL) { /* not enough or too much difference */ value = fabs (up_history_item_get_value (item) - up_history_item_get_value (item_old)); if (value < 0.01f) { item_old = NULL; goto cont; } if (value > 3.0f) { item_old = NULL; goto cont; } time_s = up_history_item_get_time (item) - up_history_item_get_time (item_old); /* use the accuracy field as a counter for now */ if ((charging && up_history_item_get_state (item) == UP_DEVICE_STATE_CHARGING) || (!charging && up_history_item_get_state (item) == UP_DEVICE_STATE_DISCHARGING)) { stats = (UpStatsItem *) g_ptr_array_index (data, bin); up_stats_item_set_value (stats, up_stats_item_get_value (stats) + time_s); up_stats_item_set_accuracy (stats, up_stats_item_get_accuracy (stats) + 1); } } item_old = item; } cont: item_last = item; } /* divide the value by the number of samples to make the average */ for (i=0; i<101; i++) { stats = (UpStatsItem *) g_ptr_array_index (data, i); if (up_stats_item_get_accuracy (stats) != 0) up_stats_item_set_value (stats, up_stats_item_get_value (stats) / up_stats_item_get_accuracy (stats)); } /* find non-zero accuracy values for the average */ for (i=0; i<101; i++) { stats = (UpStatsItem *) g_ptr_array_index (data, i); if (up_stats_item_get_accuracy (stats) > 0) { total_value += up_stats_item_get_value (stats); non_zero_accuracy++; } } /* average */ if (non_zero_accuracy != 0) average = total_value / non_zero_accuracy; g_debug ("average is %f", average); /* make the values a factor of 0, so that 1.0 is twice the * average, and -1.0 is half the average */ for (i=0; i<101; i++) { stats = (UpStatsItem *) g_ptr_array_index (data, i); if (up_stats_item_get_accuracy (stats) > 0) up_stats_item_set_value (stats, (up_stats_item_get_value (stats) - average) / average); else up_stats_item_set_value (stats, 0.0f); } /* accuracy is a percentage scale, where each cycle = 20% */ for (i=0; i<101; i++) { stats = (UpStatsItem *) g_ptr_array_index (data, i); up_stats_item_set_accuracy (stats, up_stats_item_get_accuracy (stats) * 20.0f); } return data; } /** * up_history_get_filename: **/ static gchar * up_history_get_filename (UpHistory *history, const gchar *type) { gchar *path; gchar *filename; filename = g_strdup_printf ("history-%s-%s.dat", type, history->priv->id); path = g_build_filename (history->priv->dir, filename, NULL); g_free (filename); return path; } /** * up_history_set_directory: **/ void up_history_set_directory (UpHistory *history, const gchar *dir) { g_free (history->priv->dir); history->priv->dir = g_strdup (dir); g_mkdir_with_parents (dir, 0755); } /** * up_history_array_to_file: * @list: a valid #GPtrArray instance * @filename: a filename * * Saves a copy of the list to a file **/ static gboolean up_history_array_to_file (UpHistory *history, GPtrArray *list, const gchar *filename) { guint i; UpHistoryItem *item; gchar *part; GString *string; gboolean ret = TRUE; GError *error = NULL; GTimeVal time_now; guint time_item; guint cull_count = 0; /* get current time */ g_get_current_time (&time_now); /* generate data */ string = g_string_new (""); for (i=0; ilen; i++) { item = g_ptr_array_index (list, i); /* only save entries for the last 24 hours */ time_item = up_history_item_get_time (item); if (time_now.tv_sec - time_item > history->priv->max_data_age) { cull_count++; continue; } part = up_history_item_to_string (item); if (part == NULL) { ret = FALSE; break; } g_string_append_printf (string, "%s\n", part); g_free (part); } part = g_string_free (string, FALSE); /* how many did we kill? */ g_debug ("culled %i of %i", cull_count, list->len); /* we failed to convert to string */ if (!ret) { g_warning ("failed to convert"); goto out; } /* save to disk */ ret = g_file_set_contents (filename, part, -1, &error); if (!ret) { g_warning ("failed to set data: %s", error->message); g_error_free (error); goto out; } g_debug ("saved %s", filename); out: g_free (part); return ret; } /** * up_history_array_from_file: * @list: a valid #GPtrArray instance * @filename: a filename * * Appends the list from a file **/ static gboolean up_history_array_from_file (GPtrArray *list, const gchar *filename) { gboolean ret; GError *error = NULL; gchar *data = NULL; gchar **parts = NULL; guint i; guint length; UpHistoryItem *item; /* do we exist */ ret = g_file_test (filename, G_FILE_TEST_EXISTS); if (!ret) { g_debug ("failed to get data from %s as file does not exist", filename); goto out; } /* get contents */ ret = g_file_get_contents (filename, &data, NULL, &error); if (!ret) { g_warning ("failed to get data: %s", error->message); g_error_free (error); goto out; } /* split by line ending */ parts = g_strsplit (data, "\n", 0); length = g_strv_length (parts); if (length == 0) { g_debug ("no data in %s", filename); goto out; } /* add valid entries */ g_debug ("loading %i items of data from %s", length, filename); for (i=0; ipriv->id == NULL) { g_warning ("no ID, cannot save"); goto out; } /* get filenames */ filename_rate = up_history_get_filename (history, "rate"); filename_charge = up_history_get_filename (history, "charge"); filename_time_full = up_history_get_filename (history, "time-full"); filename_time_empty = up_history_get_filename (history, "time-empty"); /* save to disk */ ret = up_history_array_to_file (history, history->priv->data_rate, filename_rate); if (!ret) goto out; ret = up_history_array_to_file (history, history->priv->data_charge, filename_charge); if (!ret) goto out; ret = up_history_array_to_file (history, history->priv->data_time_full, filename_time_full); if (!ret) goto out; ret = up_history_array_to_file (history, history->priv->data_time_empty, filename_time_empty); if (!ret) goto out; out: g_free (filename_rate); g_free (filename_charge); g_free (filename_time_full); g_free (filename_time_empty); return ret; } /** * up_history_schedule_save_cb: **/ static gboolean up_history_schedule_save_cb (UpHistory *history) { up_history_save_data (history); g_clear_pointer (&history->priv->save_source, g_source_destroy); return FALSE; } /** * up_history_is_low_power: **/ static gboolean up_history_is_low_power (UpHistory *history) { guint length; UpHistoryItem *item; /* current status is always up to date */ if (history->priv->state != UP_DEVICE_STATE_DISCHARGING) return FALSE; /* have we got any data? */ length = history->priv->data_charge->len; if (length == 0) return FALSE; /* get the last saved charge object */ item = (UpHistoryItem *) g_ptr_array_index (history->priv->data_charge, length-1); if (up_history_item_get_state (item) != UP_DEVICE_STATE_DISCHARGING) return FALSE; /* high enough */ if (up_history_item_get_value (item) > UP_HISTORY_LOW_POWER_PERCENT) return FALSE; /* we are low power */ return TRUE; } /** * up_history_schedule_save: **/ static gboolean up_history_schedule_save (UpHistory *history) { gboolean ret; gint timeout = UP_HISTORY_SAVE_INTERVAL; /* if low power, then don't batch up save requests */ ret = up_history_is_low_power (history); if (ret) { g_debug ("saving to disk earlier due to low power"); timeout = UP_HISTORY_SAVE_INTERVAL_LOW_POWER; } /* we already have one saved, clear it if it will fire earlier */ if (history->priv->save_source) { guint64 ready = g_source_get_ready_time (history->priv->save_source); if (ready > g_source_get_time (history->priv->save_source) + timeout * G_USEC_PER_SEC) { g_clear_pointer (&history->priv->save_source, g_source_destroy); } else { g_debug ("deferring as earlier timeout is already queued"); return TRUE; } } /* nothing scheduled */ g_debug ("saving in %i seconds", timeout); history->priv->save_source = g_timeout_source_new_seconds (timeout); g_source_set_name (history->priv->save_source, "[upower] up_history_schedule_save_cb"); g_source_attach (history->priv->save_source, NULL); g_source_set_callback (history->priv->save_source, (GSourceFunc) up_history_schedule_save_cb, history, NULL); return TRUE; } /** * up_history_load_data: **/ static gboolean up_history_load_data (UpHistory *history) { gchar *filename; UpHistoryItem *item; /* load rate history from disk */ filename = up_history_get_filename (history, "rate"); up_history_array_from_file (history->priv->data_rate, filename); g_free (filename); /* load charge history from disk */ filename = up_history_get_filename (history, "charge"); up_history_array_from_file (history->priv->data_charge, filename); g_free (filename); /* load charge history from disk */ filename = up_history_get_filename (history, "time-full"); up_history_array_from_file (history->priv->data_time_full, filename); g_free (filename); /* load charge history from disk */ filename = up_history_get_filename (history, "time-empty"); up_history_array_from_file (history->priv->data_time_empty, filename); g_free (filename); /* save a marker so we don't use incomplete percentages */ item = up_history_item_new (); up_history_item_set_time_to_present (item); g_ptr_array_add (history->priv->data_rate, g_object_ref (item)); g_ptr_array_add (history->priv->data_charge, g_object_ref (item)); g_ptr_array_add (history->priv->data_time_full, g_object_ref (item)); g_ptr_array_add (history->priv->data_time_empty, g_object_ref (item)); g_object_unref (item); up_history_schedule_save (history); return TRUE; } /** * up_history_set_id: **/ gboolean up_history_set_id (UpHistory *history, const gchar *id) { gboolean ret; g_return_val_if_fail (UP_IS_HISTORY (history), FALSE); if (history->priv->id != NULL) return FALSE; if (id == NULL) return FALSE; g_debug ("using id: %s", id); history->priv->id = g_strdup (id); /* load all previous data */ ret = up_history_load_data (history); return ret; } /** * up_history_set_state: **/ gboolean up_history_set_state (UpHistory *history, UpDeviceState state) { g_return_val_if_fail (UP_IS_HISTORY (history), FALSE); if (history->priv->id == NULL) return FALSE; history->priv->state = state; return TRUE; } /** * up_history_set_charge_data: **/ gboolean up_history_set_charge_data (UpHistory *history, gdouble percentage) { UpHistoryItem *item; g_return_val_if_fail (UP_IS_HISTORY (history), FALSE); if (history->priv->id == NULL) return FALSE; if (history->priv->state == UP_DEVICE_STATE_UNKNOWN) return FALSE; if (history->priv->percentage_last == percentage) return FALSE; /* add to array and schedule save file */ item = up_history_item_new (); up_history_item_set_time_to_present (item); up_history_item_set_value (item, percentage); up_history_item_set_state (item, history->priv->state); g_ptr_array_add (history->priv->data_charge, item); up_history_schedule_save (history); /* save last value */ history->priv->percentage_last = percentage; return TRUE; } /** * up_history_set_rate_data: **/ gboolean up_history_set_rate_data (UpHistory *history, gdouble rate) { UpHistoryItem *item; g_return_val_if_fail (UP_IS_HISTORY (history), FALSE); if (history->priv->id == NULL) return FALSE; if (history->priv->state == UP_DEVICE_STATE_UNKNOWN) return FALSE; if (history->priv->rate_last == rate) return FALSE; /* add to array and schedule save file */ item = up_history_item_new (); up_history_item_set_time_to_present (item); up_history_item_set_value (item, rate); up_history_item_set_state (item, history->priv->state); g_ptr_array_add (history->priv->data_rate, item); up_history_schedule_save (history); /* save last value */ history->priv->rate_last = rate; return TRUE; } /** * up_history_set_time_full_data: **/ gboolean up_history_set_time_full_data (UpHistory *history, gint64 time_s) { UpHistoryItem *item; g_return_val_if_fail (UP_IS_HISTORY (history), FALSE); if (history->priv->id == NULL) return FALSE; if (history->priv->state == UP_DEVICE_STATE_UNKNOWN) return FALSE; if (time_s < 0) return FALSE; if (history->priv->time_full_last == time_s) return FALSE; /* add to array and schedule save file */ item = up_history_item_new (); up_history_item_set_time_to_present (item); up_history_item_set_value (item, (gdouble) time_s); up_history_item_set_state (item, history->priv->state); g_ptr_array_add (history->priv->data_time_full, item); up_history_schedule_save (history); /* save last value */ history->priv->time_full_last = time_s; return TRUE; } /** * up_history_set_time_empty_data: **/ gboolean up_history_set_time_empty_data (UpHistory *history, gint64 time_s) { UpHistoryItem *item; g_return_val_if_fail (UP_IS_HISTORY (history), FALSE); if (history->priv->id == NULL) return FALSE; if (history->priv->state == UP_DEVICE_STATE_UNKNOWN) return FALSE; if (time_s < 0) return FALSE; if (history->priv->time_empty_last == time_s) return FALSE; /* add to array and schedule save file */ item = up_history_item_new (); up_history_item_set_time_to_present (item); up_history_item_set_value (item, (gdouble) time_s); up_history_item_set_state (item, history->priv->state); g_ptr_array_add (history->priv->data_time_empty, item); up_history_schedule_save (history); /* save last value */ history->priv->time_empty_last = time_s; return TRUE; } /** * up_history_class_init: * @klass: The UpHistoryClass **/ static void up_history_class_init (UpHistoryClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = up_history_finalize; } /** * up_history_init: * @history: This class instance **/ static void up_history_init (UpHistory *history) { history->priv = up_history_get_instance_private (history); history->priv->data_rate = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); history->priv->data_charge = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); history->priv->data_time_full = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); history->priv->data_time_empty = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); history->priv->max_data_age = UP_HISTORY_DEFAULT_MAX_DATA_AGE; up_history_set_directory (history, HISTORY_DIR); } /** * up_history_finalize: * @object: The object to finalize **/ static void up_history_finalize (GObject *object) { UpHistory *history; g_return_if_fail (UP_IS_HISTORY (object)); history = UP_HISTORY (object); /* save */ g_clear_pointer (&history->priv->save_source, g_source_destroy); if (history->priv->id != NULL) up_history_save_data (history); g_ptr_array_unref (history->priv->data_rate); g_ptr_array_unref (history->priv->data_charge); g_ptr_array_unref (history->priv->data_time_full); g_ptr_array_unref (history->priv->data_time_empty); g_free (history->priv->id); g_free (history->priv->dir); g_return_if_fail (history->priv != NULL); G_OBJECT_CLASS (up_history_parent_class)->finalize (object); } /** * up_history_new: * * Return value: a new UpHistory object. **/ UpHistory * up_history_new (void) { return g_object_new (UP_TYPE_HISTORY, NULL); }