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