dot/.local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io/service/plugins/battery.js

403 lines
11 KiB
JavaScript

'use strict';
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const PluginsBase = imports.service.plugins.base;
var Metadata = {
label: _('Battery'),
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 PluginsBase.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._upowerId = 0;
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';
}
icon += this.charging ? '-charging-symbolic' : '-symbolic';
return icon;
}
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]
);
}
clearCache() {
this._chargeState = [54, 0, -1];
this._dischargeState = [864, 0, -1];
this._thresholdLevel = 25;
this._estimateTime();
this.__cache_write();
}
cacheLoaded() {
this._estimateTime();
this.connected();
}
_onSendStatisticsChanged() {
if (this.settings.get_boolean('send-statistics')) {
this._monitorState();
} else {
this._unmonitorState();
}
}
handlePacket(packet) {
switch (packet.type) {
case 'kdeconnect.battery':
this._receiveState(packet);
break;
case 'kdeconnect.battery.request':
this._sendState();
break;
}
}
connected() {
super.connected();
this._requestState();
this._sendState();
}
/**
* Notify that the remote device considers the battery level low
*/
_batteryNotification(event, title, body, iconName) {
let buttons = [];
// Offer the option to ring the device, if available
if (this.device.get_action_enabled('ring')) {
buttons = [{
label: _('Ring'),
action: 'ring',
parameter: null
}];
}
this.device.showNotification({
id: `battery|${event}`,
title: title,
body: body,
icon: new Gio.ThemedIcon({name: iconName}),
buttons: buttons
});
// Save the threshold level
this._thresholdLevel = this.level;
}
_lowBatteryNotification() {
if (!this.settings.get_boolean('low-battery-notification')) {
return;
}
this._batteryNotification(
'battery|low',
// TRANSLATORS: eg. Google Pixel: Battery is low
_('%s: Battery is low').format(this.device.name),
// TRANSLATORS: eg. 15% remaining
_('%d%% remaining').format(this.level),
'battery-caution-symbolic'
);
// Save the threshold level
this._thresholdLevel = this.level;
}
_fullBatteryNotification() {
if (!this.settings.get_boolean('full-battery-notification')) {
return;
}
this._batteryNotification(
'battery|full',
// TRANSLATORS: eg. Google Pixel: Battery is full
_('%s: Battery is full').format(this.device.name),
// TRANSLATORS: when the battery is fully charged
_('Fully Charged'),
'battery-full-charged-symbolic'
);
}
/**
* Handle a remote battery update.
*
* @param {kdeconnect.battery} 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');
}
// If the level just changed to full show a notification
if (this._level === 100) {
this._fullBatteryNotification();
// Otherwise hide it
} else {
this.device.hideNotification('battery|full');
}
}
// Device considers the level low
if (packet.body.thresholdEvent > 0) {
this._lowBatteryNotification();
}
this._updateEstimate();
this.__state.state = this.state;
}
/**
* Request the remote battery's current charge/state
*/
_requestState() {
this.device.sendPacket({
type: 'kdeconnect.battery.request',
body: {request: true}
});
}
/**
* Report the local battery's current charge/state
*/
_sendState() {
if (this._upowerId === 0) {
return;
}
let upower = this.service.components.get('upower');
if (upower) {
this.device.sendPacket({
type: 'kdeconnect.battery',
body: {
currentCharge: upower.level,
isCharging: upower.charging,
thresholdEvent: upower.threshold
}
});
}
}
/**
* UPower monitoring methods
*/
_monitorState() {
try {
let upower = this.service.components.get('upower');
let incoming = this.device.settings.get_strv('incoming-capabilities');
switch (true) {
case (!incoming.includes('kdeconnect.battery')):
case (this._upowerId > 0):
case (!upower):
case (!upower.is_present):
return;
}
this._upowerId = upower.connect(
'changed',
this._sendState.bind(this)
);
this._sendState();
} catch (e) {
logError(e, this.device.name);
this._unmonitorState();
}
}
_unmonitorState() {
try {
if (this._upowerId > 0) {
let upower = this.service.components.get('upower');
if (upower) {
upower.disconnect(this._upowerId);
}
this._upowerId = 0;
}
} catch (e) {
logError(e, this.device.name);
}
}
/**
* Recalculate the (dis)charge rate and update the estimated time remaining
* See also: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/BatteryStats.java#1036
*/
_updateEstimate() {
let new_time = Math.floor(Date.now() / 1000);
let new_level = this.level;
// Read the state; rate has a default, time and level default to current
let [rate, time, level] = this.charging ? this._chargeState : this._dischargeState;
time = (Number.isFinite(time) && time > 0) ? time : new_time;
level = (Number.isFinite(level) && level > -1) ? level : new_level;
if (!Number.isFinite(rate) || rate < 1) {
rate = this.charging ? 54 : 864;
}
// Derive rate from time/level diffs (rate = seconds/percent)
let ldiff = this.charging ? new_level - level : level - new_level;
let tdiff = new_time - time;
let new_rate = tdiff / ldiff;
// Update the rate if it seems valid. Use a weighted average in favour
// of the new rate to account for possible missed level changes
if (new_rate && Number.isFinite(new_rate)) {
rate = Math.floor((rate * 0.4) + (new_rate * 0.6));
}
// Save the state
if (this.charging) {
this._chargeState = [rate, new_time, new_level];
} else {
this._dischargeState = [rate, new_time, new_level];
}
// Notify of the change
if (rate && this.charging) {
this._time = Math.floor(rate * (100 - new_level));
} else if (rate && !this.charging) {
this._time = Math.floor(rate * new_level);
}
}
/**
* Calculate and update the estimated time remaining, without affecting the
* (dis)charge rate.
*/
_estimateTime() {
// elision (rate, time, level)
let [rate,, level] = this.charging ? this._chargeState : this._dischargeState;
level = (level > -1) ? level : this.level;
if (!Number.isFinite(rate) || rate < 1) {
rate = this.charging ? 864 : 90;
}
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;
}
destroy() {
this.settings.disconnect(this._sendStatisticsId);
this._unmonitorState();
this.device.remove_action('battery');
super.destroy();
}
});