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