Branch data Line data Source code
1 : : // SPDX-License-Identifier: GPL-3.0-or-later
2 : : // SPDX-FileCopyrightText: Andy Holmes <andrew.g.r.holmes@gmail.com>
3 : :
4 : : #define G_LOG_DOMAIN "valent-battery-plugin"
5 : :
6 : : #include "config.h"
7 : :
8 : : #include <math.h>
9 : :
10 : : #include <glib/gi18n.h>
11 : : #include <gio/gio.h>
12 : : #include <valent.h>
13 : :
14 : : #include "valent-battery.h"
15 : : #include "valent-battery-plugin.h"
16 : :
17 : : /* Defaults are 90m charge, 1d discharge (seconds/percent) */
18 : : #define DEFAULT_CHARGE_RATE (90*60/100)
19 : : #define DEFAULT_DISCHARGE_RATE (24*60*60/100)
20 : :
21 : :
22 : : struct _ValentBatteryPlugin
23 : : {
24 : : ValentDevicePlugin parent_instance;
25 : :
26 : : /* Local Battery */
27 : : ValentBattery *battery;
28 : : unsigned int battery_watch : 1;
29 : :
30 : : /* Remote Battery */
31 : : gboolean charging;
32 : : const char *icon_name;
33 : : gboolean is_present;
34 : : double percentage;
35 : : int64_t time_to_full;
36 : : int64_t time_to_empty;
37 : : int64_t charge_rate;
38 : : int64_t discharge_rate;
39 : : int64_t timestamp;
40 : : };
41 : :
42 : : static const char * valent_battery_plugin_get_icon_name (ValentBatteryPlugin *self);
43 : : static void valent_battery_plugin_request_state (ValentBatteryPlugin *self);
44 : : static void valent_battery_plugin_send_state (ValentBatteryPlugin *self);
45 : :
46 [ + + + - ]: 154 : G_DEFINE_FINAL_TYPE (ValentBatteryPlugin, valent_battery_plugin, VALENT_TYPE_DEVICE_PLUGIN)
47 : :
48 : :
49 : : /*
50 : : * Local Battery
51 : : */
52 : : static void
53 : 4 : on_battery_changed (ValentBattery *battery,
54 : : ValentBatteryPlugin *self)
55 : : {
56 [ + - ]: 4 : g_assert (VALENT_IS_BATTERY (battery));
57 [ - + ]: 4 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
58 : :
59 : 4 : valent_battery_plugin_send_state (self);
60 : 4 : }
61 : :
62 : : static void
63 : 30 : valent_battery_plugin_watch_battery (ValentBatteryPlugin *self,
64 : : gboolean watch)
65 : : {
66 [ + - ]: 30 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
67 : :
68 [ + + ]: 30 : if (self->battery_watch == watch)
69 : : return;
70 : :
71 [ + + ]: 10 : if (self->battery == NULL)
72 : 5 : self->battery = valent_battery_get_default ();
73 : :
74 [ + + ]: 10 : if (watch)
75 : : {
76 : 5 : g_signal_connect_object (self->battery,
77 : : "changed",
78 : : G_CALLBACK (on_battery_changed),
79 : : self, 0);
80 : 5 : self->battery_watch = TRUE;
81 : : }
82 : : else
83 : : {
84 : 5 : g_signal_handlers_disconnect_by_data (self->battery, self);
85 : 5 : self->battery_watch = FALSE;
86 : : }
87 : : }
88 : :
89 : : static void
90 : 1 : valent_battery_plugin_handle_battery_request (ValentBatteryPlugin *self,
91 : : JsonNode *packet)
92 : : {
93 [ + - ]: 1 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
94 [ - + ]: 1 : g_assert (VALENT_IS_PACKET (packet));
95 : :
96 [ + - ]: 1 : if (valent_packet_check_field (packet, "request"))
97 : 1 : valent_battery_plugin_send_state (self);
98 : 1 : }
99 : :
100 : : static void
101 : 5 : valent_battery_plugin_send_state (ValentBatteryPlugin *self)
102 : : {
103 : 5 : g_autoptr (JsonBuilder) builder = NULL;
104 [ - + - + ]: 5 : g_autoptr (JsonNode) packet = NULL;
105 : 5 : GSettings *settings;
106 : 5 : int current_charge;
107 : 5 : gboolean is_charging;
108 : 5 : unsigned int threshold_event;
109 : :
110 [ + - ]: 5 : g_return_if_fail (VALENT_IS_BATTERY_PLUGIN (self));
111 : :
112 [ + - ]: 5 : if (!valent_battery_is_present (self->battery))
113 : : return;
114 : :
115 : : /* If the level is zero or less it's probably bogus, so send nothing */
116 [ + + ]: 5 : if (valent_battery_current_charge (self->battery) <= 0)
117 : : return;
118 : :
119 : 4 : settings = valent_extension_get_settings (VALENT_EXTENSION (self));
120 : :
121 [ + - ]: 4 : if (!g_settings_get_boolean (settings, "share-state"))
122 : : return;
123 : :
124 : 4 : current_charge = valent_battery_current_charge (self->battery);
125 : 4 : is_charging = valent_battery_is_charging (self->battery);
126 : 4 : threshold_event = valent_battery_threshold_event (self->battery);
127 : :
128 : 4 : valent_packet_init (&builder, "kdeconnect.battery");
129 : 4 : json_builder_set_member_name (builder, "currentCharge");
130 : 4 : json_builder_add_int_value (builder, current_charge);
131 : 4 : json_builder_set_member_name (builder, "isCharging");
132 : 4 : json_builder_add_boolean_value (builder, is_charging);
133 : 4 : json_builder_set_member_name (builder, "thresholdEvent");
134 : 4 : json_builder_add_int_value (builder, threshold_event);
135 : 4 : packet = valent_packet_end (&builder);
136 : :
137 [ + - ]: 4 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
138 : : }
139 : :
140 : :
141 : : /*
142 : : * Remote Battery
143 : : */
144 : : static const char *
145 : 17 : valent_battery_plugin_get_icon_name (ValentBatteryPlugin *self)
146 : : {
147 [ + + ]: 17 : if (!self->is_present)
148 : : return "battery-missing-symbolic";
149 : :
150 [ + + ]: 16 : if (self->percentage >= 100.0)
151 : : return "battery-full-charged-symbolic";
152 : :
153 [ + + ]: 14 : if (self->percentage < 5.0)
154 : 1 : return self->charging
155 : : ? "battery-empty-charging-symbolic"
156 [ - + ]: 1 : : "battery-empty-symbolic";
157 : :
158 [ + + ]: 13 : if (self->percentage < 20.0)
159 : 2 : return self->charging
160 : : ? "battery-caution-charging-symbolic"
161 [ - + ]: 2 : : "battery-caution-symbolic";
162 : :
163 [ + + ]: 11 : if (self->percentage < 30.0)
164 : 2 : return self->charging
165 : : ? "battery-low-charging-symbolic"
166 [ + + ]: 2 : : "battery-low-symbolic";
167 : :
168 [ + + ]: 9 : if (self->percentage < 60.0)
169 : 4 : return self->charging
170 : : ? "battery-good-charging-symbolic"
171 [ + + ]: 4 : : "battery-good-symbolic";
172 : :
173 : 5 : return self->charging
174 : : ? "battery-full-charging-symbolic"
175 [ + + ]: 5 : : "battery-full-symbolic";
176 : : }
177 : :
178 : : static void
179 : 16 : valent_battery_plugin_update_estimate (ValentBatteryPlugin *self,
180 : : int64_t current_charge,
181 : : gboolean is_charging)
182 : : {
183 : 16 : int64_t rate;
184 : 16 : double percentage;
185 : 16 : int64_t timestamp;
186 : :
187 [ + - ]: 16 : g_return_if_fail (current_charge >= 0);
188 : :
189 [ + - + - ]: 16 : percentage = CLAMP (current_charge, 0.0, 100.0);
190 : 16 : timestamp = floor (valent_timestamp_ms () / 1000);
191 [ + + ]: 16 : rate = is_charging ? self->charge_rate : self->discharge_rate;
192 : :
193 : : /* If the battery is present, we must have a timestamp and charge level to
194 : : * calculate the deltas and derive the (dis)charge rate. */
195 [ + + ]: 16 : if (self->is_present)
196 : : {
197 : 14 : double percentage_delta;
198 : 14 : int64_t timestamp_delta;
199 : 14 : int64_t new_rate;
200 : :
201 [ - + ]: 14 : percentage_delta = ABS (percentage - self->percentage);
202 : 14 : timestamp_delta = timestamp - self->timestamp;
203 : 14 : new_rate = timestamp_delta / percentage_delta;
204 : 14 : rate = floor ((rate * 0.4) + (new_rate * 0.6));
205 : : }
206 : :
207 : : /* Update the estimate and related values */
208 [ + + ]: 16 : if (is_charging)
209 : : {
210 : 8 : self->charge_rate = rate;
211 : 8 : self->time_to_empty = 0;
212 : 8 : self->time_to_full = floor (self->charge_rate * (100.0 - percentage));
213 : 8 : self->timestamp = timestamp;
214 : : }
215 : : else
216 : : {
217 : 8 : self->discharge_rate = rate;
218 : 8 : self->time_to_empty = floor (self->discharge_rate * percentage);
219 : 8 : self->time_to_full = 0;
220 : 8 : self->timestamp = timestamp;
221 : : }
222 : : }
223 : :
224 : : static void
225 : 28 : valent_battery_plugin_update_gaction (ValentBatteryPlugin *self)
226 : : {
227 : 28 : GVariantDict dict;
228 : 28 : GVariant *state;
229 : 28 : GAction *action;
230 : :
231 [ + - ]: 28 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
232 : :
233 : 28 : g_variant_dict_init (&dict, NULL);
234 : 28 : g_variant_dict_insert (&dict, "charging", "b", self->charging);
235 : 28 : g_variant_dict_insert (&dict, "percentage", "d", self->percentage);
236 : 28 : g_variant_dict_insert (&dict, "icon-name", "s", self->icon_name);
237 : 28 : g_variant_dict_insert (&dict, "is-present", "b", self->is_present);
238 : 28 : g_variant_dict_insert (&dict, "time-to-empty", "x", self->time_to_empty);
239 : 28 : g_variant_dict_insert (&dict, "time-to-full", "x", self->time_to_full);
240 : 28 : state = g_variant_dict_end (&dict);
241 : :
242 : : /* Update the state, even if we're disabling the action */
243 : 28 : action = g_action_map_lookup_action (G_ACTION_MAP (self), "state");
244 : 28 : g_simple_action_set_enabled (G_SIMPLE_ACTION (action), self->is_present);
245 : 28 : g_simple_action_set_state (G_SIMPLE_ACTION (action), state);
246 : 28 : }
247 : :
248 : : static void
249 : 17 : valent_battery_plugin_update_notification (ValentBatteryPlugin *self,
250 : : int threshold_event)
251 : : {
252 : 15 : g_autoptr (GNotification) notification = NULL;
253 [ + - ]: 17 : g_autofree char *title = NULL;
254 : 17 : g_autofree char *body = NULL;
255 : 17 : g_autoptr (GIcon) icon = NULL;
256 : 17 : ValentDevice *device;
257 : 17 : const char *device_name;
258 : 17 : GSettings *settings;
259 : 17 : double full, low;
260 : :
261 [ + - ]: 17 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
262 : :
263 : 17 : device = valent_extension_get_object (VALENT_EXTENSION (self));
264 : 17 : device_name = valent_device_get_name (device);
265 : 17 : settings = valent_extension_get_settings (VALENT_EXTENSION (self));
266 : :
267 : 17 : full = g_settings_get_double (settings, "full-notification-level");
268 : 17 : low = g_settings_get_double (settings, "low-notification-level");
269 : :
270 [ + + ]: 17 : if (self->percentage >= full)
271 : : {
272 [ - + ]: 2 : if (!g_settings_get_boolean (settings, "full-notification"))
273 : : return;
274 : :
275 : : /* TRANSLATORS: This is <device name>: Fully Charged */
276 : 0 : title = g_strdup_printf (_("%s: Fully Charged"), device_name);
277 : : /* TRANSLATORS: When the battery level is at maximum */
278 [ # # ]: 0 : body = g_strdup (_("Battery Fully Charged"));
279 : 0 : icon = g_themed_icon_new ("battery-full-charged-symbolic");
280 : : }
281 : :
282 : : /* Battery is no longer low or is charging */
283 [ + + + + ]: 15 : else if (self->percentage > low || self->charging)
284 : : {
285 : 13 : valent_device_plugin_hide_notification (VALENT_DEVICE_PLUGIN (self),
286 : : "battery-level");
287 : 13 : return;
288 : : }
289 : :
290 : : /* Battery is now low */
291 [ - + - - ]: 2 : else if (self->percentage <= low || threshold_event == 1)
292 : : {
293 : 2 : int64_t total_minutes;
294 : 2 : int minutes;
295 : 2 : int hours;
296 : :
297 [ + - ]: 2 : if (!g_settings_get_boolean (settings, "low-notification"))
298 : : return;
299 : :
300 : 2 : total_minutes = floor (self->time_to_empty / 60);
301 : 2 : minutes = total_minutes % 60;
302 : 2 : hours = floor (total_minutes / 60);
303 : :
304 : : /* TRANSLATORS: This is <device name>: Battery Low */
305 : 2 : title = g_strdup_printf (_("%s: Battery Low"), device_name);
306 : : /* TRANSLATORS: This is <percentage> (<hours>:<minutes> Remaining) */
307 : 2 : body = g_strdup_printf (_("%g%% (%d∶%02d Remaining)"),
308 : : self->percentage, hours, minutes);
309 : 2 : icon = g_themed_icon_new ("battery-caution-symbolic");
310 : : }
311 : :
312 : 2 : notification = g_notification_new (title);
313 : 2 : g_notification_set_body (notification, body);
314 : 2 : g_notification_set_icon (notification, icon);
315 : :
316 [ + - ]: 2 : valent_device_plugin_show_notification (VALENT_DEVICE_PLUGIN (self),
317 : : "battery-level",
318 : : notification);
319 : : }
320 : :
321 : : static void
322 : 18 : valent_battery_plugin_handle_battery (ValentBatteryPlugin *self,
323 : : JsonNode *packet)
324 : : {
325 : 18 : gboolean is_charging;
326 : 18 : int64_t current_charge;
327 : 18 : int64_t threshold_event;
328 : :
329 [ + - ]: 18 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
330 [ - + ]: 18 : g_assert (VALENT_IS_PACKET (packet));
331 : :
332 [ - + ]: 18 : if (!valent_packet_get_boolean (packet, "isCharging", &is_charging))
333 : 0 : is_charging = self->charging;
334 : :
335 [ - + ]: 18 : if (!valent_packet_get_int (packet, "currentCharge", ¤t_charge))
336 : 0 : current_charge = self->percentage;
337 : :
338 [ - + ]: 18 : if (!valent_packet_get_int (packet, "thresholdEvent", &threshold_event))
339 : 0 : threshold_event = 0;
340 : :
341 : : /* We get a lot of battery updates, so check if something changed */
342 [ + + + + ]: 24 : if (self->charging == is_charging &&
343 [ + + ]: 6 : G_APPROX_VALUE (self->percentage, current_charge, 0.1))
344 : 1 : return;
345 : :
346 : : /* If `current_charge` is `-1`, either there is no battery or statistics are
347 : : * unavailable. Otherwise update the estimate before the instance properties
348 : : * so that the time/percentage deltas can be calculated. */
349 [ + + ]: 17 : if (current_charge >= 0)
350 : 16 : valent_battery_plugin_update_estimate (self, current_charge, is_charging);
351 : :
352 : 17 : self->charging = is_charging;
353 [ + - + + ]: 17 : self->percentage = CLAMP (current_charge, 0.0, 100.0);
354 : 17 : self->is_present = current_charge >= 0;
355 : 17 : self->icon_name = valent_battery_plugin_get_icon_name (self);
356 : :
357 : 17 : valent_battery_plugin_update_gaction (self);
358 : 17 : valent_battery_plugin_update_notification (self, threshold_event);
359 : : }
360 : :
361 : : static void
362 : 5 : valent_battery_plugin_request_state (ValentBatteryPlugin *self)
363 : : {
364 : 10 : g_autoptr (JsonBuilder) builder = NULL;
365 [ - + ]: 5 : g_autoptr (JsonNode) packet = NULL;
366 : :
367 [ + - ]: 5 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
368 : :
369 : 5 : valent_packet_init (&builder, "kdeconnect.battery.request");
370 : 5 : json_builder_set_member_name (builder, "request");
371 : 5 : json_builder_add_boolean_value (builder, TRUE);
372 : 5 : packet = valent_packet_end (&builder);
373 : :
374 [ + - ]: 5 : valent_device_plugin_queue_packet (VALENT_DEVICE_PLUGIN (self), packet);
375 : 5 : }
376 : :
377 : : /*
378 : : * GActions
379 : : */
380 : : static void
381 : 0 : state_action (GSimpleAction *action,
382 : : GVariant *parameter,
383 : : gpointer user_data)
384 : : {
385 : : // No-op to make the state read-only
386 : 0 : }
387 : :
388 : : static const GActionEntry actions[] = {
389 : : {"state", NULL, NULL, "@a{sv} {}", state_action},
390 : : };
391 : :
392 : : /*
393 : : * ValentDevicePlugin
394 : : */
395 : : static void
396 : 18 : valent_battery_plugin_update_state (ValentDevicePlugin *plugin,
397 : : ValentDeviceState state)
398 : : {
399 : 18 : ValentBatteryPlugin *self = VALENT_BATTERY_PLUGIN (plugin);
400 : 18 : gboolean available;
401 : :
402 [ + - ]: 18 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
403 : :
404 : 18 : available = (state & VALENT_DEVICE_STATE_CONNECTED) != 0 &&
405 : : (state & VALENT_DEVICE_STATE_PAIRED) != 0;
406 : :
407 [ + + ]: 18 : if (available)
408 : : {
409 : 5 : valent_battery_plugin_update_gaction (self);
410 : 5 : valent_battery_plugin_watch_battery (self, TRUE);
411 : 5 : valent_battery_plugin_request_state (self);
412 : : }
413 : : else
414 : : {
415 : 13 : valent_extension_toggle_actions (VALENT_EXTENSION (plugin), available);
416 : 13 : valent_battery_plugin_watch_battery (self, FALSE);
417 : : }
418 : 18 : }
419 : :
420 : : static void
421 : 19 : valent_battery_plugin_handle_packet (ValentDevicePlugin *plugin,
422 : : const char *type,
423 : : JsonNode *packet)
424 : : {
425 : 19 : ValentBatteryPlugin *self = VALENT_BATTERY_PLUGIN (plugin);
426 : :
427 [ + - ]: 19 : g_assert (VALENT_IS_BATTERY_PLUGIN (self));
428 [ - + ]: 19 : g_assert (type != NULL);
429 [ - + ]: 19 : g_assert (VALENT_IS_PACKET (packet));
430 : :
431 : : /* The remote battery state changed */
432 [ + + ]: 19 : if (g_str_equal (type, "kdeconnect.battery"))
433 : 18 : valent_battery_plugin_handle_battery (self, packet);
434 : :
435 : : /* A request for the local battery state */
436 [ + - ]: 1 : else if (g_str_equal (type, "kdeconnect.battery.request"))
437 : 1 : valent_battery_plugin_handle_battery_request (self, packet);
438 : :
439 : : else
440 : 0 : g_assert_not_reached ();
441 : 19 : }
442 : :
443 : : /*
444 : : * GObject
445 : : */
446 : : static void
447 : 6 : valent_battery_plugin_constructed (GObject *object)
448 : : {
449 : 6 : ValentBatteryPlugin *self = VALENT_BATTERY_PLUGIN (object);
450 : :
451 : 6 : g_action_map_add_action_entries (G_ACTION_MAP (object),
452 : : actions,
453 : : G_N_ELEMENTS (actions),
454 : : object);
455 : 6 : valent_battery_plugin_update_gaction (self);
456 : :
457 : 6 : G_OBJECT_CLASS (valent_battery_plugin_parent_class)->constructed (object);
458 : 6 : }
459 : :
460 : : static void
461 : 12 : valent_battery_plugin_dispose (GObject *object)
462 : : {
463 : 12 : ValentBatteryPlugin *self = VALENT_BATTERY_PLUGIN (object);
464 : :
465 : 12 : valent_battery_plugin_watch_battery (self, FALSE);
466 : :
467 : 12 : G_OBJECT_CLASS (valent_battery_plugin_parent_class)->dispose (object);
468 : 12 : }
469 : :
470 : : static void
471 : 3 : valent_battery_plugin_class_init (ValentBatteryPluginClass *klass)
472 : : {
473 : 3 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
474 : 3 : ValentDevicePluginClass *plugin_class = VALENT_DEVICE_PLUGIN_CLASS (klass);
475 : :
476 : 3 : object_class->constructed = valent_battery_plugin_constructed;
477 : 3 : object_class->dispose = valent_battery_plugin_dispose;
478 : :
479 : 3 : plugin_class->handle_packet = valent_battery_plugin_handle_packet;
480 : 3 : plugin_class->update_state = valent_battery_plugin_update_state;
481 : : }
482 : :
483 : : static void
484 : 6 : valent_battery_plugin_init (ValentBatteryPlugin *self)
485 : : {
486 : 6 : self->icon_name = "battery-missing-symbolic";
487 : 6 : self->charge_rate = DEFAULT_CHARGE_RATE;
488 : 6 : self->discharge_rate = DEFAULT_DISCHARGE_RATE;
489 : 6 : }
490 : :
|