/* GKrellM
|  Copyright (C) 1999-2021 Bill Wilson
|
|  Author:  Bill Wilson
|  Latest versions might be found at:  https://gkrellm.srcbox.net
|
|
|  GKrellM 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.
|
|  GKrellM 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, see http://www.gnu.org/licenses/
|
|
|  Additional permission under GNU GPL version 3 section 7
|
|  If you modify this program, or any covered work, by linking or
|  combining it with the OpenSSL project's OpenSSL library (or a
|  modified version of that library), containing parts covered by
|  the terms of the OpenSSL or SSLeay licenses, you are granted
|  additional permission to convey the resulting work.
|  Corresponding Source for a non-source form of such a combination
|  shall include the source code for the parts of OpenSSL used as well
|  as that of the covered work.
*/

#include "gkrellm.h"
#include "gkrellm-private.h"
#include "gkrellm-sysdeps.h"

#include <math.h>


#define	BAT_CONFIG_KEYWORD	"battery"

typedef enum
	{
	BATTERYDISPLAY_PERCENT,
	BATTERYDISPLAY_TIME,
	BATTERYDISPLAY_RATE,
	BATTERYDISPLAY_EOM	/* end of modes */
	}
	BatteryDisplayMode;


typedef struct
	{
	gint				id;
	GkrellmPanel		*panel;
	GkrellmKrell		*krell;
	GkrellmDecal		*power_decal;
	GkrellmDecal		*time_decal;

	GkrellmAlert		*alert;

	gboolean			enabled;

	BatteryDisplayMode	display_mode;
	gfloat				charge_rate;	/* % / min */

	gboolean			present,
						on_line,
						charging;
	gint				percent;
	gint				time_left;		/* In minutes, -1 if minutes unavail */
	}
	Battery;

static GList			*battery_list;
static GkrellmMonitor	*mon_battery;
static GtkWidget		*battery_vbox;

static Battery			*composite_battery,
						*launch_battery;

static gboolean			enable_composite,
						enable_each,
						enable_estimate;

static gint				poll_interval = 5,
						full_cap_fallback = 5000;

static GkrellmLauncher	launch;

static GkrellmAlert		*bat_alert;		/* One alert dupped for each battery */

static gint				style_id;

static gboolean			alert_units_percent,
						alert_units_need_estimate_mode;

static GkrellmBatteryReadFunc read_battery_data;
static void		create_battery_panel(Battery *bat, gboolean first_create);

static gint		n_batteries;


static Battery *
battery_nth(gint n, gboolean create)
	{
	Battery			*bat;

	if (n > 10)
		return NULL;
	if (n < 0)
		{
		if (!composite_battery && create)
			{
			bat = g_new0(Battery, 1);
			battery_list = g_list_prepend(battery_list, bat);
			bat->id = GKRELLM_BATTERY_COMPOSITE_ID;		/* -1 */
			composite_battery = bat;
			gkrellm_alert_dup(&bat->alert, bat_alert);
			}
		return composite_battery;
		}

	if (composite_battery)
		++n;

	while (   (bat = (Battery *) g_list_nth_data(battery_list, n)) == NULL
		   && create
		  )
		{
		bat = g_new0(Battery, 1);
		battery_list = g_list_append(battery_list, bat);
		bat->id = n_batteries++;
		gkrellm_alert_dup(&bat->alert, bat_alert);
		}
	return bat;
	}


  /* Themers need to be able to see the battery monitor.
  */
static void
read_battery_demo(void)
	{
	gboolean	on_line, charging;
	gint		percent, time_left;
	static gint	bump = 60;

	if (bump <= 5)
		bump = 60;
	bump -= 5;
	on_line = bump > 45;
	if (on_line)
		{
		charging = TRUE;
		time_left = 200 + (60 - bump) * 20;
		percent = time_left / 5;
		}
	else
		{
		charging = FALSE;
		time_left = bump;
		percent = 1 + bump;
		}
	gkrellm_battery_assign_data(0, TRUE, on_line, charging,
				percent, time_left);
	}


static gboolean
setup_battery_interface(void)
	{
	if (!read_battery_data && !_GK.client_mode && gkrellm_sys_battery_init())
		read_battery_data = gkrellm_sys_battery_read_data;
	if (_GK.demo)
		read_battery_data = read_battery_demo;
	return read_battery_data ? TRUE : FALSE;
	}

void
gkrellm_battery_client_divert(GkrellmBatteryReadFunc read_func)
	{
	read_battery_data = read_func;
	}

void
gkrellm_battery_assign_data(gint n, gboolean present, gboolean on_line,
			gboolean charging, gint percent, gint time_left)
	{
	Battery	*bat;

	bat = battery_nth(n, TRUE);
	if (!bat)
		return;
	bat->present = present;
	bat->on_line = on_line;
	bat->charging = charging;
	bat->percent = percent;
	bat->time_left = time_left;
	}

  /* Help out some laptops with Linux ACPI bugs */
gint
gkrellm_battery_full_cap_fallback(void)
	{
	return full_cap_fallback;
	}


/* -------------------------------------------------------------- */


/* estimate (guess-timate?) battery time remaining, based on the rate of
   discharge (and conversely the time to charge based on the rate of charge).
  - some BIOS' only provide battery levels, not any estimate of the time
    remaining

  Battery charge/discharge characteristics (or, why dc/dt doesn't really work)
  - the charge/discharge curves of most battery types tend to be very non-
    linear (http://www.google.com/search?q=battery+charge+discharge+curve)
  - on discharge, most battery types will initially fall somewhat rapidly
    from 100 percent, then flatten out and stay somewhat linear until
    suddenly "dropping out" when nearly depleted (approx. 10-20% capacity).
    For practical purposes we can consider this point to be the end of the
    discharge curve. This is simple enough to model via a fixed capacity
    offset to cut out just at the knee of this curve, and allows us to
    reasonably approximate the rest of the curve by a linear function
    and simple dc/dt calculation.
  - with regard to charging, however, it's not quite so easy. With a
    constant voltage charger, the battery capacity rises exponentially
    (charging current decreases as battery terminal voltage rises). The
    final stages of charging are very gradual, with a relatively long
    period at "almost but not quite 100%".

    Unfortunately a linear extrapolation at the beginning of an
    exponential curve will be a poor approximation to the true expected
    time to charge, tending to be significantly undervalued. Using an
    exponential model to estimate time to approx. 90-95% (2.5 * exp. time
    constant) seems to give a more reasonable fit. That said, the poor
    relative resolution at higher charge values makes estimating the
    exponential time constant difficult towards the end of the charge
    cycle (the curve's very nearly flat). So, I've settled on a mixed
    model - for c < ~70 I use an exponential model, and switch to linear
    above that (or if the charge rate seems to have otherwise "flatlined").

    Empirically, this method seems to give reasonable results [1] -
    certainly  much better than seeing "0:50:00 to full" for a good half an
    hour (i.e. as happens with apmd, which uses a linear model for both
    charging + discharging). Note that a constant-current charger should
    be pretty well linear all the way along the charge curve, which means
    the linear rate extrapolation should work well in this case. The user
    can choose which model they wish to use via estimate_model.

    [1] I logged my Compaq Armada M300's capacity (via /proc/apm) over one
    complete discharge/charge cycle (machine was idle the whole time). The
    discharge curve was linear to approx. 14% when the BIOS alerted of
    impending doom; upon plugging in the external power supply the capacity
    rose exponentially to 100%, with a time constant of approx. 0.8 hr (i.e.
    approx. 2+ hrs to full charge).

  Linear rate of change calculation:
  - in an ideal, continuous world, estimated time to 0(100) would simply
    be the remaining capacity divided by the charge rate
       ttl = c / dc(t)/dt
  - alas, the reported battery capacity is bound to integer values thus
    c(t) is a discontinuous function. i.e. has fairly large steps. And of
    course then dc/dt is undefined at the discontinuities.
  - to avoid this issue the rate of charge is determined by the deltas from
    the start of the last state change (charge/discharge cycle) (time T)
       ttl(t) = c(t) / ((C - c(t)) / (T - t))    C = charge at time T
    Furthermore, the rate changes are windowed and filtered to mitigate
    c(t) transients (e.g. at the start of discharge) and smooth over
    discontinuities (and fudge for battery characteristics, ref. above).
*/

#define BAT_SLEEP_DETECT 300		/* interval of >300s => must have slept */
#define BAT_DISCHARGE_TRANSIENT 10	/* ignore first 10% of discharge cycle */
#define BAT_EMPTY_CAPACITY 12		/* when is the battery "flat"? */
#define BAT_RATECHANGE_WINDOW 90	/* allow rate changes after 90s */
#define BAT_CHARGE_MODEL_LIMIT 60	/* linear charge model cutover point */
#define BAT_RATE_SMOOTHING 0.3		/* rate smoothing weight */

/* #define BAT_ESTIMATE_DEBUG */

  /* user-provided nominal battery runtimes, hrs (used to determine initial
  |  discharge, stable, charge rate (%/min))
  */
static gfloat	estimate_runtime[2] = {0};
static gint		estimate_model[2] = {0};
static gboolean	reset_estimate;

static void
estimate_battery_time_left(Battery *bat)
	{
	/* ?0 = at time 0 (state change); ?1 = at last "sample" (rate change) */
	static time_t	t0 = -1, t1;
	static gint		p0, p1;
	static gint		c0;
	static gfloat	rate = 0;
	static time_t	dt;
	static gint		dp;
	time_t			t = time(NULL);

	/* 1 charging; 0 power on and stable; -1 discharging */
	gint			charging = bat->charging ? 1 : (bat->on_line ? 0 : -1);

#ifdef BAT_ESTIMATE_DEBUG
		fprintf(stderr, "%ld bc?=%d ac?=%d (%+d) bp=%d\t", t,
			bat->charging, bat->on_line, charging,
			bat->percent);
#endif

	if (   reset_estimate || t0 < 0 || c0 != charging
		|| (t - t1) > BAT_SLEEP_DETECT
	   )
		{
		/* init, state change, or sleep/hibernation
		*/
		reset_estimate = FALSE;
		c0 = charging;

		t0 = t1 = t;
		if (charging < 0 && (bat->percent > 100 - BAT_DISCHARGE_TRANSIENT))
			p0 = p1 = 100 - BAT_DISCHARGE_TRANSIENT;
		else
			p0 = p1 = bat->percent;
		dp = dt = 0;
		rate = 0.0;

		/* convert runtime (hrs) to signed rate (%/min)
		*/
		if (charging < 0)
			rate = -100 / (estimate_runtime[0] * 60);
		else if (charging > 0)
			rate =  100 / (estimate_runtime[1] * 60);

#ifdef BAT_ESTIMATE_DEBUG
		fprintf(stderr, "[r = %.3f]\t", rate);
#endif
		}
	else
		{
		time_t	dt1 = t - t1;		/* delta since last rate change */
		gint	dp1 = bat->percent - p1;

		/* time for a rate change?
		*/
		if (   dt1 > BAT_RATECHANGE_WINDOW
			&& ((charging > 0 && dp1 >= 0) || (charging < 0 && dp1 <= 0))
		   )
			{
			dt = t - t0;					/* since state change */
			dp = bat->percent - p0;

			if (dp1 == 0)	/* flatlining (dp/dt = 0) */
				rate = (1 - BAT_RATE_SMOOTHING/4) * rate;
			else
				rate = BAT_RATE_SMOOTHING *
						((gdouble) dp / (gdouble) (dt/60)) +
						(1 - BAT_RATE_SMOOTHING) * rate;

#ifdef BAT_ESTIMATE_DEBUG
			fprintf(stderr, "%d [dp = %+d dt = %.2f rate = %.3f]\t",
					(gint) dp1, dp, (gdouble) dt / 60, rate);
#endif

			t1 = t;
			p1 = bat->percent;
			}
		}

	if (charging && rate != 0.0)	/* (dis)charging */
		{
		gfloat	eta;
		gint 	p = charging > 0 ? 100 - bat->percent :
						bat->percent - BAT_EMPTY_CAPACITY;

		if (   charging > 0 && estimate_model[1]
			&& bat->percent < BAT_CHARGE_MODEL_LIMIT && dp > 0
		   )
			/* charging, use exponential: eta =~ 2.5 * time-constant (~=92%) */
			eta = -2.5 * dt/60 / (log(1 - (gdouble)dp/(gdouble)(p+dp)));
		else
			eta = fabs((gdouble)p / rate);	/* use linear */

#ifdef BAT_ESTIMATE_DEBUG
		fprintf(stderr, "eta = %.2f\t", eta);
#endif

		/* round off to nearest 5 mins */
		bat->time_left = (gint)((eta > 0 ? eta + 2.5: 0) / 5) * 5;
		bat->charge_rate = rate;
		}
	else
		{
		bat->time_left = INT_MAX;	/* inf */
		bat->charge_rate = 0.0;
		}

#ifdef BAT_ESTIMATE_DEBUG
		fprintf(stderr, "\n");
#endif
	}

static void
draw_time_left_decal(Battery *bat, gboolean force)
	{
	GkrellmDecal	*d;
	gchar			buf[16];
	gint			x, w, t;
	static BatteryDisplayMode	last_mode = BATTERYDISPLAY_EOM;

	if (!bat->panel)
		return;

	BatteryDisplayMode battery_display_mode = bat->display_mode;
	if (bat->time_left == -1)
		battery_display_mode = BATTERYDISPLAY_PERCENT;
	if (last_mode != battery_display_mode)
		force = TRUE;
	last_mode = bat->display_mode;

	switch (battery_display_mode)
		{
		case BATTERYDISPLAY_TIME:
			t = bat->time_left;
			if (t == INT_MAX || t == INT_MIN)
				snprintf(buf, sizeof(buf), "--");
			else
				snprintf(buf, sizeof(buf), "%2d:%02d", t / 60, t % 60);
			break;

		case BATTERYDISPLAY_RATE:
			/* t is used by draw_decal_text() to see if a refresh is reqd */
			t = (gint) (bat->charge_rate * 100.0);
			snprintf(buf, sizeof(buf), "%0.1f%%/m",
						bat->charge_rate);
			break;

		case BATTERYDISPLAY_PERCENT:
		default:
			t = bat->percent;
			if (t == -1)	/* APM battery flags should cause hide... but */
				snprintf(buf, sizeof(buf), "no bat");
			else
				snprintf(buf, sizeof(buf), "%d%%", t);
			break;
		}

	d = bat->time_decal;
	w = gkrellm_gdk_string_width(d->text_style.font, buf);
	x = (d->w - w) / 2;
	if (x < 0)
		x = 0;
	d->x_off = x;
	gkrellm_draw_decal_text(bat->panel, d, buf, force ? -1 : t);
	}

static void
update_battery_panel(Battery *bat)
	{
	GkrellmPanel	*p  = bat->panel;

	if (!p)
		return;
	if (!bat->present)
		{	/* Battery can be removed while running */
		gkrellm_panel_hide(p);
		return;
		}
	gkrellm_panel_show(p);

	if (bat->time_left > 0 && bat->charging)
		bat->charge_rate = (gfloat) (100 - bat->percent) / (gfloat) bat->time_left;
	else
		bat->charge_rate = 0.0;

	if (enable_estimate)
		estimate_battery_time_left(bat);

	if (bat->on_line)
		{
		gkrellm_reset_alert(bat->alert);
		gkrellm_freeze_alert(bat->alert);
		gkrellm_draw_decal_pixmap(p, bat->power_decal, D_MISC_AC);
		}
	else
		{
		if (   (bat == composite_battery && enable_composite)
			|| (bat != composite_battery && enable_each)
			)
			{
			gkrellm_thaw_alert(bat->alert);
			gkrellm_check_alert(bat->alert, alert_units_percent
						? bat->percent : bat->time_left);
			}
		gkrellm_draw_decal_pixmap(p, bat->power_decal, D_MISC_BATTERY);
		}
	draw_time_left_decal(bat, FALSE);
	gkrellm_update_krell(p, bat->krell, bat->percent);
	gkrellm_draw_panel_layers(p);
	}

static void
update_battery(void)
	{
	GList		*list;
	Battery		*bat;
	static gint	seconds = 0;

	if (!enable_each && !enable_composite)
		return;
	if (GK.second_tick)
		{
		if (seconds == 0)
			{
			for (list = battery_list; list; list = list->next)
				((Battery *) list->data)->present = FALSE;

			(*read_battery_data)();
			for (list = battery_list; list; list = list->next)
				{
				bat = (Battery *) list->data;
				if (!bat->panel)
					create_battery_panel(bat, TRUE);
				if (bat->enabled)
					update_battery_panel(bat);
				}
			}
		seconds = (seconds + 1) % poll_interval;
		}
	}

static gboolean
cb_expose_event(GtkWidget *widget, GdkEventExpose *ev, GkrellmPanel *p)
	{
	gdk_draw_drawable(gtk_widget_get_window(widget),
                          gtk_widget_get_style(widget)->fg_gc[gtk_widget_get_state(widget)], p->pixmap,
			ev->area.x, ev->area.y, ev->area.x, ev->area.y,
			ev->area.width, ev->area.height);
	return FALSE;
	}

static gboolean
cb_panel_enter(GtkWidget *w, GdkEventButton *ev, Battery *bat)
	{
	gkrellm_decal_on_top_layer(bat->time_decal, TRUE);
	gkrellm_draw_panel_layers(bat->panel);
	return FALSE;
	}

static gboolean
cb_panel_leave(GtkWidget *w, GdkEventButton *ev, Battery *bat)
	{
	gkrellm_decal_on_top_layer(bat->time_decal, FALSE);
	gkrellm_draw_panel_layers(bat->panel);
	return FALSE;
	}


static gboolean
cb_panel_press(GtkWidget *widget, GdkEventButton *ev, Battery *bat)
	{
	GkrellmDecal			*d;
	static gboolean	time_unavailable_warned;

	d = launch.decal;
	if (ev->button == 3)
		gkrellm_open_config_window(mon_battery);
	else if (   ev->button == 2
			 || (ev->button == 1 && !d)
			 || (ev->button == 1 && d && ev->x < d->x)
			)
		{
		if (bat->time_left == -1 && bat->present)
			{
			if (!time_unavailable_warned)
				gkrellm_message_dialog(_("GKrellM Battery"),
					_("Battery times are unavailable.  You\n"
					  "could select the Estimated Time option."));
			time_unavailable_warned = TRUE;
			bat->display_mode = BATTERYDISPLAY_PERCENT;
			}
		else
			{
			bat->display_mode++;
			if (bat->display_mode == BATTERYDISPLAY_EOM)
				bat->display_mode = 0;

			draw_time_left_decal(bat, TRUE);
			gkrellm_draw_panel_layers(bat->panel);
			gkrellm_config_modified();
			}
		}
	return FALSE;
	}

static void
create_battery_panel(Battery *bat, gboolean first_create)
	{
	GkrellmPanel		*p;
	GkrellmStyle		*style;
	GkrellmMargin		*m;
	gint				x, w;

	if (!bat->panel)
		bat->panel = gkrellm_panel_new0();
	p = bat->panel;
	style = gkrellm_meter_style(style_id);
	m = gkrellm_get_style_margins(style);
	bat->power_decal = gkrellm_create_decal_pixmap(p,
			gkrellm_decal_misc_pixmap(), gkrellm_decal_misc_mask(),
			N_MISC_DECALS, style, m->left, -1);

	x = bat->power_decal->x + bat->power_decal->w + 2;
	w = gkrellm_chart_width() - x - m->right;
	bat->time_decal = gkrellm_create_decal_text(p, "8/%",
						gkrellm_meter_textstyle(style_id),
						style, x, -1, w);

	bat->krell = gkrellm_create_krell(p,
						gkrellm_krell_meter_piximage(style_id), style);
	gkrellm_monotonic_krell_values(bat->krell, FALSE);
	gkrellm_set_krell_full_scale(bat->krell, 100, 1);

	gkrellm_panel_configure(p, NULL, style);
	gkrellm_panel_create(battery_vbox, mon_battery, p);

	/* Center the decals with respect to each other.
	*/
	if (bat->power_decal->h > bat->time_decal->h)
		bat->time_decal->y += (bat->power_decal->h - bat->time_decal->h) / 2;
	else
		bat->power_decal->y += (bat->time_decal->h - bat->power_decal->h) / 2;

	if (first_create)
		{
		g_signal_connect(G_OBJECT(p->drawing_area), "expose-event",
				G_CALLBACK(cb_expose_event), p);
		g_signal_connect(G_OBJECT(p->drawing_area), "button-press-event",
				G_CALLBACK(cb_panel_press), bat);
		g_signal_connect(G_OBJECT(p->drawing_area), "enter-notify-event",
                G_CALLBACK(cb_panel_enter), bat);
		g_signal_connect(G_OBJECT(p->drawing_area), "leave-notify-event",
                G_CALLBACK(cb_panel_leave), bat);
		 }

	gkrellm_setup_decal_launcher(p, &launch, bat->time_decal);
	if (   (bat == composite_battery && enable_composite)
		|| (bat->id == 0 && composite_battery && !enable_composite)
		|| (bat->id == 0 && !composite_battery)
		)
		launch_battery = bat;

	if (bat == composite_battery)
		bat->enabled = enable_composite;
	else
		bat->enabled = enable_each;

	if (bat->enabled)
		update_battery_panel(bat);
	else
		gkrellm_panel_hide(p);
	}

static void
spacer_visibility(void)
	{
	GList		*list;
	Battery		*bat;
	gboolean	enabled = FALSE;

	for (list = battery_list; list; list = list->next)
		{
		bat = (Battery *) list->data;
		enabled |= bat->enabled;
		}
	if (enabled)
		gkrellm_spacers_show(mon_battery);
	else
		gkrellm_spacers_hide(mon_battery);
	}

static void
create_battery(GtkWidget *vbox, gint first_create)
	{
	GList	*list;
	Battery	*bat;

	battery_vbox = vbox;
	if (_GK.demo)
		enable_each = TRUE;
	for (list = battery_list; list; list = list->next)
		{
		bat = (Battery *) list->data;
		create_battery_panel(bat, first_create);
		}
	spacer_visibility();
	}


  /* Expand alert command substitution variables:
  |  $H - hostname           $n - battery id
  |  $t - time left          $p - percent
  |  $o - online (boolean)   $c - charging (boolean)
  */
static void
cb_command_process(GkrellmAlert *alert, gchar *src, gchar *buf, gint size,
			Battery *bat)
	{
	gchar		c, *s;
	gint		len, value;

	if (!buf || size < 1)
		return;
	--size;
	*buf = '\0';
	if (!src)
		return;
	for (s = src; *s != '\0' && size > 0; ++s)
		{
		len = 1;
		if (*s == '$' && *(s + 1) != '\0')
			{
			value = -1;
			if ((c = *(s + 1)) == 'H')
				len = snprintf(buf, size, "%s", gkrellm_sys_get_host_name());
			else if (c == 'n' && bat != composite_battery)
				value = bat->id;
			else if (c == 't')
				value = bat->time_left;
			else if (c == 'p')
				value = bat->percent;
			else if (c == 'o')
				value = bat->on_line;
			else if (c == 'c')
				value = bat->charging;
			else
				len = 0;

			if (value >= 0)
				len = snprintf(buf, size, "%d", value);
			++s;
			}
		else
			*buf = *s;
		size -= len;
		buf += len;
		}
	*buf = '\0';
	}

static void
cb_battery_alert_trigger(GkrellmAlert *alert, Battery *bat)
	{
	GkrellmAlertdecal	*ad;
	GkrellmDecal		*d;

	alert->panel = bat->panel;
	ad = &alert->ad;
	d = bat->time_decal;
	ad->x = d->x + 1;
	ad->y = d->y - 2;
	ad->w = d->w - 1;
	ad->h = d->h + 4;
	gkrellm_render_default_alert_decal(alert);
	}

static void
dup_battery_alert(void)
	{
	GList	*list;
	Battery	*bat;

	for (list = battery_list; list; list = list->next)
		{
		bat = (Battery *) list->data;
		gkrellm_alert_dup(&bat->alert, bat_alert);
		gkrellm_alert_trigger_connect(bat->alert,
					cb_battery_alert_trigger, bat);
		gkrellm_alert_command_process_connect(bat->alert,
					cb_command_process, bat);
		}
	}


  /* If the OS reports battery times, alerts will always have minutes units.
  |  If the OS does not report battery times the initial alert create will
  |  have minutes units if the estimate time option is enabled and it will
  |  have battery percent level units if estimate time option is off.  Alert
  |  creates from load config will have units in effect at last save config.
  */
static void
create_alert(void)
	{
	Battery	*bat;

	if (!battery_list)
		return;
	bat = (Battery *) battery_list->data;

	if (!bat_alert)
		{
		alert_units_need_estimate_mode = FALSE;

		if (   alert_units_percent
			|| (bat->time_left == -1 && !enable_estimate)
		   )
			{
			if (bat->time_left == -1)
				alert_units_percent = TRUE;
			bat_alert = gkrellm_alert_create(NULL, _("Battery"),
					_("Battery Percent Limits"),
					FALSE, TRUE, TRUE, 99, 0, 1, 10, 0);
			}
		else
			{
			bat_alert = gkrellm_alert_create(NULL, _("Battery"),
					_("Battery Minutes Remaining Limits"),
					FALSE, TRUE, TRUE, 500, 0, 1, 10, 0);
			if (bat->time_left == -1)
				alert_units_need_estimate_mode = TRUE;
			}
		}
	gkrellm_alert_config_connect(bat_alert, dup_battery_alert, NULL);

	/* This alert is a master to be dupped and is itself never checked */
	}


static void
save_battery_config(FILE *f)
	{
	GList	*list;
	Battery	*bat;

	fprintf(f, "%s enable %d\n", BAT_CONFIG_KEYWORD, enable_each);
	fprintf(f, "%s enable_composite %d\n", BAT_CONFIG_KEYWORD,
					enable_composite);
	fprintf(f, "%s estimate_time %d\n", BAT_CONFIG_KEYWORD, enable_estimate);

	/* 2.1.15: scale saved float values to avoid decimal points in the config.
	*/
	fprintf(f, "%s estimate_time_discharge %.0f\n", BAT_CONFIG_KEYWORD,
				estimate_runtime[0] * GKRELLM_FLOAT_FACTOR);
	fprintf(f, "%s estimate_time_charge %.0f\n", BAT_CONFIG_KEYWORD,
				estimate_runtime[1] * GKRELLM_FLOAT_FACTOR);
	fprintf(f, "%s estimate_time_charge_model %d\n", BAT_CONFIG_KEYWORD,
				estimate_model[1]);
	fprintf(f, "%s full_cap_fallback %d\n", BAT_CONFIG_KEYWORD,
						full_cap_fallback);
	if (!_GK.client_mode)
		fprintf(f, "%s poll_interval %d\n", BAT_CONFIG_KEYWORD, poll_interval);
	if (launch.command)
		fprintf(f, "%s launch1 %s\n", BAT_CONFIG_KEYWORD, launch.command);
	if (launch.tooltip_comment)
		fprintf(f, "%s tooltip_comment %s\n",
					BAT_CONFIG_KEYWORD, launch.tooltip_comment);
	fprintf(f, "%s alert_units_percent %d\n", BAT_CONFIG_KEYWORD,
				alert_units_percent);
	for (list = battery_list; list; list = list->next)
		{
		bat = (Battery *) list->data;
		if (bat == composite_battery)	/* Don't 2.1.16 backwards break */
			fprintf(f, "%s display_mode_composite %d %d\n", BAT_CONFIG_KEYWORD,
						bat->display_mode, bat->id);
		else
			fprintf(f, "%s display_mode %d %d\n", BAT_CONFIG_KEYWORD,
						bat->display_mode, bat->id);
		}
	gkrellm_save_alertconfig(f, bat_alert, BAT_CONFIG_KEYWORD, NULL);
	}

static void
load_battery_config(gchar *arg)
	{
	Battery			*bat;
	gint			display_mode, n = 0;
	gchar			config[32], item[CFG_BUFSIZE],
					name[CFG_BUFSIZE], item1[CFG_BUFSIZE];

	if (sscanf(arg, "%31s %[^\n]", config, item) == 2)
		{
		if (!strcmp(config, "enable"))
			sscanf(item, "%d", &enable_each);
		if (!strcmp(config, "enable_composite"))
			sscanf(item, "%d", &enable_composite);
		else if (!strcmp(config, "estimate_time"))
			sscanf(item, "%d", &enable_estimate);
		else if (!strcmp(config, "estimate_time_discharge"))
			{
			sscanf(item, "%f", &estimate_runtime[0]);
			estimate_runtime[0] /= _GK.float_factor;
			}
		else if (!strcmp(config, "estimate_time_charge"))
			{
			sscanf(item, "%f", &estimate_runtime[1]);
			estimate_runtime[1] /= _GK.float_factor;
			}
		else if (!strcmp(config, "estimate_time_charge_model"))
			sscanf(item, "%d", &estimate_model[1]);
		else if (!strcmp(config, "full_cap_fallback"))
			sscanf(item, "%d", &full_cap_fallback);
		else if (!strcmp(config, "poll_interval"))
			sscanf(item, "%d", &poll_interval);
		else if (!strcmp(config, "launch1"))
			launch.command = g_strdup(item);
		else if (!strcmp(config, "tooltip_comment"))
			launch.tooltip_comment = g_strdup(item);
		else if (!strncmp(config, "display_mode", 12))
			{
			sscanf(item, "%d %d", &display_mode, &n);
			if ((bat = battery_nth(n, FALSE)) != NULL)
				bat->display_mode = display_mode;
			}
		else if (!strcmp(config, "alert_units_percent"))
			sscanf(item, "%d", &alert_units_percent);
		else if (!strcmp(config, GKRELLM_ALERTCONFIG_KEYWORD))
			{
			if (!strncmp(item, "BAT", 3))	/* Config compat musical chairs */
				sscanf(item, "%32s %[^\n]", name, item1);
			else
				strcpy(item1, item);
			create_alert();
			gkrellm_load_alertconfig(&bat_alert, item1);
			dup_battery_alert();
			}
		}
	}


static GtkWidget	*launch_entry,
					*tooltip_entry;

static GtkWidget	*estimate_runtime_spin_button[2],
					*estimate_model_button[2];

static void
update_battery_panels(void)
	{
	GList		*list;
	Battery		*bat;

	for (list = battery_list; list; list = list->next)
		{
		bat = (Battery *) list->data;
		if (bat->enabled)
			update_battery_panel(bat);
		}
	}

static void
cb_set_alert(GtkWidget *button, Battery *bat)
	{
	create_alert();
	gkrellm_alert_config_window(&bat_alert);
	}

static void
alert_units_percent_cb(GtkToggleButton *button, gpointer data)
	{
	GList	*list;
	Battery	*bat;

	alert_units_percent = gtk_toggle_button_get_active(button);

	if (bat_alert)
		{
		for (list = battery_list; list; list = list->next)
			{
			bat = (Battery *) list->data;
			gkrellm_reset_alert(bat->alert);
			gkrellm_alert_destroy(&bat->alert);
			}
		gkrellm_alert_destroy(&bat_alert);
		gkrellm_config_message_dialog(_("GKrellM Battery"),
				_("The Battery alert units are changed\n"
				  "and the alert must be reconfigured."));
		}
	}

static void
cb_enable_estimate(GtkToggleButton *button, GtkWidget *box)
	{
	GList		*list;
	Battery		*bat;
	gboolean	enable;

	enable = gtk_toggle_button_get_active(button);
	gtk_widget_set_sensitive(box, enable);

	if (enable_estimate != enable)
		{
		/* If alert units need estimated time mode and estimation switches off,
		|  destroy the alert because the alert units can now only be percent.
		*/
		for (list = battery_list; list; list = list->next)
			{
			bat = (Battery *) list->data;
			if (bat->alert && (!enable && alert_units_need_estimate_mode))
				gkrellm_alert_destroy(&bat->alert);
			}
		if (   bat_alert
		    && (!enable && alert_units_need_estimate_mode)
		    && !alert_units_percent
		   )
			{
			gkrellm_alert_destroy(&bat_alert);
			gkrellm_config_message_dialog(_("GKrellM Battery"),
					_("The Battery alert units are changed\n"
					  "and the alert must be reconfigured."));
			}
		}
	enable_estimate = enable;
	update_battery_panels();
	}

static void
cb_runtime(GtkWidget *entry, gpointer data)
	{
	gint	i = GPOINTER_TO_INT(data) - 1;

	estimate_runtime[i] = gtk_spin_button_get_value(
			GTK_SPIN_BUTTON(estimate_runtime_spin_button[i]));
	reset_estimate = TRUE;
	update_battery_panels();
	}


static void
cb_enable(GtkToggleButton *button, gpointer data)
	{
	GList	*list;
	Battery	*bat;
	gint	which  = GPOINTER_TO_INT(data);

	if (which == 0)
		enable_composite = enable_each = gtk_toggle_button_get_active(button);
	else if (which == 1)
		enable_each = gtk_toggle_button_get_active(button);
	else if (which == 2)
		enable_composite = gtk_toggle_button_get_active(button);

	for (list = battery_list; list; list = list->next)
		{
		bat = (Battery *) list->data;
		if (bat == composite_battery)
			bat->enabled = enable_composite;
		else
			bat->enabled = enable_each;

		if (bat->enabled)
			{
			gkrellm_panel_show(bat->panel);
			update_battery_panel(bat);
			}
		else
			{
			gkrellm_reset_alert(bat->alert);
			gkrellm_panel_hide(bat->panel);
			}
		}
	if (composite_battery)
		{
		gkrellm_remove_launcher(&launch);

		if (composite_battery->enabled)
			{
			gkrellm_setup_decal_launcher(composite_battery->panel,
						&launch, composite_battery->time_decal);
			launch_battery = composite_battery;
			}
		else
			{
			bat = battery_nth(0, FALSE);
			if (bat && bat->enabled)
				{
				gkrellm_setup_decal_launcher(bat->panel,
						&launch, bat->time_decal);
				launch_battery = bat;
				}
			}

		}
	spacer_visibility();
	}

static void
cb_estimate_model(GtkWidget *entry, gpointer data)
	{
	gint	i = GPOINTER_TO_INT(data);

	estimate_model[i] =
			gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(estimate_model_button[i]));
	reset_estimate = TRUE;
	update_battery_panels();
	}

static void
cb_poll_interval(GtkWidget *entry, GtkSpinButton *spin)
	{
	poll_interval = gtk_spin_button_get_value_as_int(spin);
	}

static void
cb_launch_entry(GtkWidget *widget, gpointer data)
	{
	if (!launch_battery)
		return;
	gkrellm_apply_launcher(&launch_entry, &tooltip_entry,
				launch_battery->panel, &launch, gkrellm_launch_button_cb);
	}


static gchar	*battery_info_text[] =
{
N_("<h>Setup\n"),
N_("<b>Display Estimated Time\n"),
N_("If battery times are not reported by the BIOS or if the reported times\n"
"are inaccurate, select this option to display a battery time remaining or\n"
"time to charge which is calculated based on the current battery percentage\n"
"level, user supplied total battery times, and a linear extrapolation model.\n"),
"\n",
N_("<b>Total Battery Times\n"),
N_("Enter the typical total run time and total charge time in hours for your\n"
"battery.\n"),
"\n",
N_("<b>Exponential Charge Model\n"),		/* xgettext:no-c-format */
N_("For some charging systems battery capacity rises exponentially, which\n"
"means the simple linear model will grossly underestimate the time to 100%.\n"
"Select the exponential model for more accuracy in this case.\n"),
"\n",
"<b>",
N_("Alerts"),
"\n",
N_("Substitution variables may be used in alert commands.\n"),
N_("\t$p    battery percent level.\n"),
N_("\t$t    battery time left.\n"),
N_("\t$n    battery number.\n"),
N_("\t$o    online state (boolean).\n"),
N_("\t$c    charging state (boolean).\n"),
"\n",
N_("<h>Mouse Button Actions:\n"),
N_("<b>\tLeft "),
N_(" click on the charging state decal to toggle the display mode\n"
"\t\tbetween a minutes, percentage, or charging rate display.\n"),
N_("<b>\tMiddle "),
N_(" clicking anywhere on the Battery panel also toggles the display mode.\n")
};

static void
create_battery_tab(GtkWidget *tab_vbox)
	{
	GtkWidget	*tabs, *table, *vbox, *vbox1, *vbox2,
				*hbox, *hbox2, *text;
	Battery		*bat;

	tabs = gtk_notebook_new();
	gtk_notebook_set_tab_pos(GTK_NOTEBOOK(tabs), GTK_POS_TOP);
	gtk_box_pack_start(GTK_BOX(tab_vbox), tabs, TRUE, TRUE, 0);

/* -- Setup tab */
	vbox = gkrellm_gtk_notebook_page(tabs, _("Options"));
	vbox = gkrellm_gtk_framed_vbox(vbox, NULL, 2, TRUE, 10, 6);

	if (composite_battery && n_batteries > 0)
		{
		vbox1 = gkrellm_gtk_category_vbox(vbox, _("Enable"), 2, 2, TRUE);

		gkrellm_gtk_check_button_connected(vbox1, NULL,
				enable_composite, FALSE, FALSE, 0,
				cb_enable, GINT_TO_POINTER(2),
				_("Composite Battery"));
		gkrellm_gtk_check_button_connected(vbox1, NULL,
				enable_each, FALSE, FALSE, 0,
				cb_enable, GINT_TO_POINTER(1),
				_("Real Batteries"));
		}
	else
		gkrellm_gtk_check_button_connected(vbox, NULL,
				enable_each, FALSE, FALSE, 10,
				cb_enable, GINT_TO_POINTER(0),
				_("Enable Battery"));

	vbox2 = gtk_vbox_new(FALSE, 0);
	gtk_box_pack_start(GTK_BOX(vbox), vbox2, FALSE, FALSE, 0);

	vbox1 = gtk_vbox_new(FALSE, 0);
	gkrellm_gtk_check_button_connected(vbox2, NULL,
			enable_estimate, FALSE, FALSE, 2,
			cb_enable_estimate, vbox1,
			_("Display estimated time remaining and time to charge"));
	gtk_widget_set_sensitive(vbox1, enable_estimate ? TRUE : FALSE);
	gtk_box_pack_start(GTK_BOX(vbox2), vbox1, FALSE, FALSE, 0);

	vbox1 = gkrellm_gtk_category_vbox(vbox1, NULL, 0, 0, TRUE);
	gkrellm_gtk_spin_button(vbox1, &estimate_runtime_spin_button[0],
			estimate_runtime[0], 0.1, 24, 0.1, 1.0, 1, 55,
			cb_runtime, GINT_TO_POINTER(1), FALSE,
			_("Total battery run time in hours"));
	gkrellm_gtk_spin_button(vbox1, &estimate_runtime_spin_button[1],
			estimate_runtime[1], 0.1, 24, 0.1, 1.0, 1, 55,
			cb_runtime, GINT_TO_POINTER(2), FALSE,
			_("Total battery charge time in hours"));

	gkrellm_gtk_check_button_connected(vbox1, &estimate_model_button[1],
            estimate_model[1], FALSE, FALSE, 0,
			cb_estimate_model, GINT_TO_POINTER(1),
			_("Exponential charge model"));

	if (!_GK.client_mode)
		{
		hbox2 = gtk_hbox_new(FALSE, 0);
		gkrellm_gtk_spin_button(hbox2, NULL,
				(gfloat) poll_interval, 1, 3600, 1, 10, 0, 55,
				cb_poll_interval, NULL, FALSE,
				_("Seconds between updates"));
		gtk_box_pack_end(GTK_BOX(vbox), hbox2, FALSE, FALSE, 6);
		}

	vbox = gkrellm_gtk_framed_notebook_page(tabs, _("Setup"));

	vbox1 = gkrellm_gtk_category_vbox(vbox,
				_("Launch Commands"),
				4, 0, TRUE);
	table = gkrellm_gtk_launcher_table_new(vbox1, 1);
	gkrellm_gtk_config_launcher(table, 0, &launch_entry, &tooltip_entry,
					_("Battery"), &launch);
	g_signal_connect(G_OBJECT(launch_entry), "changed",
			G_CALLBACK(cb_launch_entry), NULL);
	g_signal_connect(G_OBJECT(tooltip_entry), "changed",
			G_CALLBACK(cb_launch_entry), NULL);

	hbox = gtk_hbox_new(FALSE, 0);
	gtk_box_pack_end(GTK_BOX(vbox), hbox, FALSE, FALSE, 8);
	gkrellm_gtk_alert_button(hbox, NULL, FALSE, FALSE, 4, TRUE,
				cb_set_alert, NULL);
	if (battery_list)
		{
		bat = (Battery *) battery_list->data;
		if (bat && bat->time_left >= 0)		/* No choice if no battery times */
			gkrellm_gtk_check_button_connected(hbox, NULL,
					alert_units_percent, FALSE, FALSE, 16,
					alert_units_percent_cb, NULL,
					_("Alerts check for percent capacity remaining."));
		}


/* --Info tab */
	vbox = gkrellm_gtk_framed_notebook_page(tabs, _("Info"));
	text = gkrellm_gtk_scrolled_text_view(vbox, NULL,
				GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
	for (size_t i = 0; i < G_N_ELEMENTS(battery_info_text); ++i)
		gkrellm_gtk_text_view_append(text, _(battery_info_text[i]));
	}

static GkrellmMonitor	monitor_battery =
	{
	N_("Battery"),			/* Name, for config tab.	*/
	MON_BATTERY,			/* Id, 0 if a plugin		*/
	create_battery,			/* The create function		*/
	update_battery,			/* The update function		*/
	create_battery_tab,		/* The config tab create function	*/
	NULL,				/* Apply the config function		*/

	save_battery_config,	/* Save user config			*/
	load_battery_config,	/* Load user config			*/
	BAT_CONFIG_KEYWORD,	/* config keyword			*/

	NULL,				/* Undef 2	*/
	NULL,				/* Undef 1	*/
	NULL,				/* Undef 0	*/

	0,					/* insert_before_id - place plugin before this mon */

	NULL,				/* Handle if a plugin, filled in by GKrellM		*/
	NULL				/* path if a plugin, filled in by GKrellM		*/
	};

GkrellmMonitor *
gkrellm_init_battery_monitor(void)
	{
	estimate_runtime[0] =  1.5;	/* 1.5 hour battery */
	estimate_runtime[1] =	3.0;	/* 3 hour recharge */
	if (_GK.client_mode)
		poll_interval = 1;

	monitor_battery.name=_(monitor_battery.name);

	style_id = gkrellm_add_meter_style(&monitor_battery, BATTERY_STYLE_NAME);
	mon_battery = &monitor_battery;
	if (setup_battery_interface())
		{
		(*read_battery_data)();
		return &monitor_battery;
		}
	return NULL;
	}
