123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
- //
- // SPDX-License-Identifier: GPL-2.0-or-later
- 'use strict';
- const Gio = imports.gi.Gio;
- const GLib = imports.gi.GLib;
- const GObject = imports.gi.GObject;
- const Components = imports.service.components;
- const PluginBase = imports.service.plugin;
- var Metadata = {
- label: _('Battery'),
- description: _('Exchange battery information'),
- id: 'org.gnome.Shell.Extensions.GSConnect.Plugin.Battery',
- incomingCapabilities: [
- 'kdeconnect.battery',
- 'kdeconnect.battery.request',
- ],
- outgoingCapabilities: [
- 'kdeconnect.battery',
- 'kdeconnect.battery.request',
- ],
- actions: {},
- };
- /**
- * Battery Plugin
- * https://github.com/KDE/kdeconnect-kde/tree/master/plugins/battery
- */
- var Plugin = GObject.registerClass({
- GTypeName: 'GSConnectBatteryPlugin',
- }, class Plugin extends PluginBase.Plugin {
- _init(device) {
- super._init(device, 'battery');
- // Setup Cache; defaults are 90 minute charge, 1 day discharge
- this._chargeState = [54, 0, -1];
- this._dischargeState = [864, 0, -1];
- this._thresholdLevel = 25;
- this.cacheProperties([
- '_chargeState',
- '_dischargeState',
- '_thresholdLevel',
- ]);
- // Export battery state as GAction
- this.__state = new Gio.SimpleAction({
- name: 'battery',
- parameter_type: new GLib.VariantType('(bsii)'),
- state: this.state,
- });
- this.device.add_action(this.__state);
- // Local Battery (UPower)
- this._upower = null;
- this._sendStatisticsId = this.settings.connect(
- 'changed::send-statistics',
- this._onSendStatisticsChanged.bind(this)
- );
- this._onSendStatisticsChanged(this.settings);
- }
- get charging() {
- if (this._charging === undefined)
- this._charging = false;
- return this._charging;
- }
- get icon_name() {
- let icon;
- if (this.level === -1)
- return 'battery-missing-symbolic';
- else if (this.level === 100)
- return 'battery-full-charged-symbolic';
- else if (this.level < 3)
- icon = 'battery-empty';
- else if (this.level < 10)
- icon = 'battery-caution';
- else if (this.level < 30)
- icon = 'battery-low';
- else if (this.level < 60)
- icon = 'battery-good';
- else if (this.level >= 60)
- icon = 'battery-full';
- if (this.charging)
- return `${icon}-charging-symbolic`;
- return `${icon}-symbolic`;
- }
- get level() {
- // This is what KDE Connect returns if the remote battery plugin is
- // disabled or still being loaded
- if (this._level === undefined)
- this._level = -1;
- return this._level;
- }
- get time() {
- if (this._time === undefined)
- this._time = 0;
- return this._time;
- }
- get state() {
- return new GLib.Variant(
- '(bsii)',
- [this.charging, this.icon_name, this.level, this.time]
- );
- }
- cacheLoaded() {
- this._initEstimate();
- this._sendState();
- }
- clearCache() {
- this._chargeState = [54, 0, -1];
- this._dischargeState = [864, 0, -1];
- this._thresholdLevel = 25;
- this._initEstimate();
- }
- connected() {
- super.connected();
- this._requestState();
- this._sendState();
- }
- handlePacket(packet) {
- switch (packet.type) {
- case 'kdeconnect.battery':
- this._receiveState(packet);
- break;
- case 'kdeconnect.battery.request':
- this._sendState();
- break;
- }
- }
- _onSendStatisticsChanged() {
- if (this.settings.get_boolean('send-statistics'))
- this._monitorState();
- else
- this._unmonitorState();
- }
- /**
- * Recalculate and update the estimated time remaining, but not the rate.
- */
- _initEstimate() {
- let rate, level;
- // elision of [rate, time, level]
- if (this.charging)
- [rate,, level] = this._chargeState;
- else
- [rate,, level] = this._dischargeState;
- if (!Number.isFinite(rate) || rate < 1)
- rate = this.charging ? 864 : 90;
- if (!Number.isFinite(level) || level < 0)
- level = this.level;
- // Update the time remaining
- if (rate && this.charging)
- this._time = Math.floor(rate * (100 - level));
- else if (rate && !this.charging)
- this._time = Math.floor(rate * level);
- this.__state.state = this.state;
- }
- /**
- * Recalculate the (dis)charge rate and update the estimated time remaining.
- */
- _updateEstimate() {
- let rate, time, level;
- const newTime = Math.floor(Date.now() / 1000);
- const newLevel = this.level;
- // Load the state; ensure we have sane values for calculation
- if (this.charging)
- [rate, time, level] = this._chargeState;
- else
- [rate, time, level] = this._dischargeState;
- if (!Number.isFinite(rate) || rate < 1)
- rate = this.charging ? 54 : 864;
- if (!Number.isFinite(time) || time <= 0)
- time = newTime;
- if (!Number.isFinite(level) || level < 0)
- level = newLevel;
- // Update the rate; use a weighted average to account for missed changes
- // NOTE: (rate = seconds/percent)
- const ldiff = this.charging ? newLevel - level : level - newLevel;
- const tdiff = newTime - time;
- const newRate = tdiff / ldiff;
- if (newRate && Number.isFinite(newRate))
- rate = Math.floor((rate * 0.4) + (newRate * 0.6));
- // Store the state for the next recalculation
- if (this.charging)
- this._chargeState = [rate, newTime, newLevel];
- else
- this._dischargeState = [rate, newTime, newLevel];
- // Update the time remaining
- if (rate && this.charging)
- this._time = Math.floor(rate * (100 - newLevel));
- else if (rate && !this.charging)
- this._time = Math.floor(rate * newLevel);
- this.__state.state = this.state;
- }
- /**
- * Notify the user the remote battery is full.
- */
- _fullBatteryNotification() {
- if (!this.settings.get_boolean('full-battery-notification'))
- return;
- // Offer the option to ring the device, if available
- let buttons = [];
- if (this.device.get_action_enabled('ring')) {
- buttons = [{
- label: _('Ring'),
- action: 'ring',
- parameter: null,
- }];
- }
- this.device.showNotification({
- id: 'battery|full',
- // TRANSLATORS: eg. Google Pixel: Battery is full
- title: _('%s: Battery is full').format(this.device.name),
- // TRANSLATORS: when the battery is fully charged
- body: _('Fully Charged'),
- icon: Gio.ThemedIcon.new('battery-full-charged-symbolic'),
- buttons: buttons,
- });
- }
- /**
- * Notify the user the remote battery is at custom charge level.
- */
- _customBatteryNotification() {
- if (!this.settings.get_boolean('custom-battery-notification'))
- return;
- // Offer the option to ring the device, if available
- let buttons = [];
- if (this.device.get_action_enabled('ring')) {
- buttons = [{
- label: _('Ring'),
- action: 'ring',
- parameter: null,
- }];
- }
- this.device.showNotification({
- id: 'battery|custom',
- // TRANSLATORS: eg. Google Pixel: Battery has reached custom charge level
- title: _('%s: Battery has reached custom charge level').format(this.device.name),
- // TRANSLATORS: when the battery has reached custom charge level
- body: _('%d%% Charged').format(this.level),
- icon: Gio.ThemedIcon.new('battery-full-charged-symbolic'),
- buttons: buttons,
- });
- }
- /**
- * Notify the user the remote battery is low.
- */
- _lowBatteryNotification() {
- if (!this.settings.get_boolean('low-battery-notification'))
- return;
- // Offer the option to ring the device, if available
- let buttons = [];
- if (this.device.get_action_enabled('ring')) {
- buttons = [{
- label: _('Ring'),
- action: 'ring',
- parameter: null,
- }];
- }
- this.device.showNotification({
- id: 'battery|low',
- // TRANSLATORS: eg. Google Pixel: Battery is low
- title: _('%s: Battery is low').format(this.device.name),
- // TRANSLATORS: eg. 15% remaining
- body: _('%d%% remaining').format(this.level),
- icon: Gio.ThemedIcon.new('battery-caution-symbolic'),
- buttons: buttons,
- });
- }
- /**
- * Handle a remote battery update.
- *
- * @param {Core.Packet} packet - A kdeconnect.battery packet
- */
- _receiveState(packet) {
- // Charging state changed
- this._charging = packet.body.isCharging;
- // Level changed
- if (this._level !== packet.body.currentCharge) {
- this._level = packet.body.currentCharge;
- // If the level is above the threshold hide the notification
- if (this._level > this._thresholdLevel)
- this.device.hideNotification('battery|low');
- // The level just changed to/from custom level while charging
- if ((this._level === this.settings.get_uint('custom-battery-notification-value')) && this._charging)
- this._customBatteryNotification();
- else
- this.device.hideNotification('battery|custom');
- // The level just changed to/from full
- if (this._level === 100)
- this._fullBatteryNotification();
- else
- this.device.hideNotification('battery|full');
- }
- // Device considers the level low
- if (packet.body.thresholdEvent > 0) {
- this._lowBatteryNotification();
- this._thresholdLevel = this.level;
- }
- this._updateEstimate();
- }
- /**
- * Request the remote battery's current state
- */
- _requestState() {
- this.device.sendPacket({
- type: 'kdeconnect.battery.request',
- body: {request: true},
- });
- }
- /**
- * Report the local battery's current state
- */
- _sendState() {
- if (this._upower === null || !this._upower.is_present)
- return;
- this.device.sendPacket({
- type: 'kdeconnect.battery',
- body: {
- currentCharge: this._upower.level,
- isCharging: this._upower.charging,
- thresholdEvent: this._upower.threshold,
- },
- });
- }
- /*
- * UPower monitoring methods
- */
- _monitorState() {
- try {
- // Currently only true if the remote device is a desktop (rare)
- const incoming = this.device.settings.get_strv('incoming-capabilities');
- if (!incoming.includes('kdeconnect.battery'))
- return;
- this._upower = Components.acquire('upower');
- this._upowerId = this._upower.connect(
- 'changed',
- this._sendState.bind(this)
- );
- this._sendState();
- } catch (e) {
- logError(e, this.device.name);
- this._unmonitorState();
- }
- }
- _unmonitorState() {
- try {
- if (this._upower === null)
- return;
- this._upower.disconnect(this._upowerId);
- this._upower = Components.release('upower');
- } catch (e) {
- logError(e, this.device.name);
- }
- }
- destroy() {
- this.device.remove_action('battery');
- this.settings.disconnect(this._sendStatisticsId);
- this._unmonitorState();
- super.destroy();
- }
- });
|