828 lines
26 KiB
JavaScript
828 lines
26 KiB
JavaScript
/**
|
|
* Driver for handling thresholds in Lenovo ThinkPad series for models since 2011
|
|
*
|
|
* Based on information from https://linrunner.de/tlp/faq/battery.html
|
|
*
|
|
* Original sources: https://github.com/linrunner/TLP/blob/main/bat.d/05-thinkpad
|
|
*/
|
|
'use strict';
|
|
|
|
import GLib from 'gi://GLib';
|
|
import GObject from 'gi://GObject';
|
|
import Gio from 'gi://Gio';
|
|
|
|
/*
|
|
* On some models the start threshold 0 is not allowed, instead 95 is used (Ex: E14 Gen 3)
|
|
* See issues:
|
|
* #8: https://gitlab.com/marcosdalvarez/thinkpad-battery-threshold-extension/-/issues/8
|
|
* #20: https://gitlab.com/marcosdalvarez/thinkpad-battery-threshold-extension/-/issues/20
|
|
*/
|
|
const ALTERNATIVE_DISABLED_START = /(Think[pP]ad) (E14 Gen 3|P14s Gen 2a)/;
|
|
|
|
// Driver constants
|
|
const BASE_PATH = '/sys/class/power_supply';
|
|
const START_FILE_OLD = 'charge_start_threshold'; // kernel 4.17 and newer
|
|
const END_FILE_OLD = 'charge_stop_threshold'; // kernel 4.17 and newer
|
|
const START_FILE_NEW = 'charge_control_start_threshold'; // kernel 5.9 and newer
|
|
const END_FILE_NEW = 'charge_control_end_threshold'; // kernel 5.9 and newer
|
|
|
|
/**
|
|
* Read file contents
|
|
*
|
|
* @param {string} path Path of file
|
|
* @returns {string} File contents
|
|
*/
|
|
const readFile = function (path) {
|
|
try {
|
|
const f = Gio.File.new_for_path(path);
|
|
const [, contents,] = f.load_contents(null);
|
|
const decoder = new TextDecoder('utf-8');
|
|
return decoder.decode(contents);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read integer value from file
|
|
*
|
|
* @param {string} path Path of file
|
|
* @returns {number|null} Return a integer or null
|
|
*/
|
|
const readFileInt = function (path) {
|
|
try {
|
|
const v = readFile(path);
|
|
if (v) {
|
|
return parseInt(v);
|
|
} else {
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test file/directory exists
|
|
*
|
|
* @param {string} path File/directory path
|
|
* @returns {boolean}
|
|
*/
|
|
const fileExists = function (path) {
|
|
try {
|
|
const f = Gio.File.new_for_path(path);
|
|
return f.query_exists(null);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Environment object
|
|
*/
|
|
const Environment = GObject.registerClass({
|
|
GTypeName: 'Environment',
|
|
}, class Environment extends GObject.Object {
|
|
|
|
get productVersion() {
|
|
if (this._productVersion === undefined) {
|
|
const tmp = readFile('/sys/class/dmi/id/product_version');
|
|
if (tmp) {
|
|
// Remove non-alphanumeric characters
|
|
const sanitize = /([^ A-Za-z0-9])+/g;
|
|
this._productVersion = tmp.replace(sanitize, '');
|
|
} else {
|
|
this._productVersion = null;
|
|
}
|
|
}
|
|
return this._productVersion;
|
|
}
|
|
|
|
get kernelRelease() {
|
|
if (this._kernelRelease === undefined) {
|
|
try {
|
|
let proc = Gio.Subprocess.new(
|
|
['uname', '-r'],
|
|
Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
|
|
);
|
|
const [ok, stdout, stderr] = proc.communicate_utf8(null, null);
|
|
proc = null;
|
|
this._kernelRelease = stdout.trim();
|
|
} catch (e) {
|
|
logError(e);
|
|
}
|
|
}
|
|
return this._kernelRelease;
|
|
}
|
|
|
|
get kernelMajorVersion() {
|
|
if (this._kernelMajorVersion === undefined) {
|
|
[this._kernelMajorVersion, this._kernelMinorVersion] = this.kernelRelease.split('.', 2);
|
|
}
|
|
return this._kernelMajorVersion;
|
|
}
|
|
|
|
get kernelMinorVersion() {
|
|
if (this._kernelMinorVersion === undefined) {
|
|
[this._kernelMajorVersion, this._kernelMinorVersion] = this.kernelRelease.split('.', 2);
|
|
}
|
|
return this._kernelMinorVersion;
|
|
}
|
|
|
|
checkMinKernelVersion(major, minor) {
|
|
return (
|
|
this.kernelMajorVersion > major ||
|
|
(this.kernelMajorVersion === major && this.kernelMinorVersion >= minor)
|
|
);
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
* Execute a command and generate events based on its result
|
|
*
|
|
* @param {string} command Command to execute
|
|
* @param {boolean} asRoot True to run with elevated permissions
|
|
*/
|
|
const Runnable = GObject.registerClass({
|
|
GTypeName: 'Runnable',
|
|
Signals: {
|
|
'command-completed': {
|
|
param_types: [GObject.TYPE_JSOBJECT /* error */]
|
|
}
|
|
}
|
|
}, class Runnable extends GObject.Object {
|
|
constructor(command, asRoot = false) {
|
|
super();
|
|
if (!command) {
|
|
throw Error('The command cannot be an empty string');
|
|
}
|
|
this._command = command;
|
|
this._asRoot = asRoot;
|
|
}
|
|
|
|
run() {
|
|
const argv = ['sh', '-c', this._command];
|
|
|
|
if (this._asRoot) {
|
|
argv.unshift('pkexec');
|
|
}
|
|
|
|
try {
|
|
const [, pid] = GLib.spawn_async(null, argv, null, GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, null);
|
|
|
|
GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) => {
|
|
try {
|
|
GLib.spawn_check_exit_status(status);
|
|
this.emit('command-completed', null);
|
|
} catch (e) {
|
|
if (e.code == 126) {
|
|
// Cancelled
|
|
} else {
|
|
logError(e);
|
|
this.emit('command-completed', e);
|
|
}
|
|
}
|
|
GLib.spawn_close_pid(pid);
|
|
});
|
|
} catch (e) {
|
|
logError(e);
|
|
this.emit('command-completed', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
/**
|
|
* ThinkPad battery object
|
|
*/
|
|
export const ThinkPadBattery = GObject.registerClass({
|
|
GTypeName: 'ThinkPadBattery',
|
|
Properties: {
|
|
'environment': GObject.ParamSpec.object(
|
|
'environment',
|
|
'Environment',
|
|
'Environment',
|
|
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
|
Environment.$gtype
|
|
),
|
|
'name': GObject.ParamSpec.string(
|
|
'name',
|
|
'Name',
|
|
'Battery name',
|
|
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
|
null
|
|
),
|
|
'auth-required': GObject.ParamSpec.boolean(
|
|
'auth-required',
|
|
'Auth required',
|
|
'Authorization is required to write the values',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'is-active': GObject.ParamSpec.boolean(
|
|
'is-active',
|
|
'Is active',
|
|
'Indicates if the thresholds are active or not',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'is-available': GObject.ParamSpec.boolean(
|
|
'is-available',
|
|
'Is available',
|
|
'Indicates if the battery are available or not',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'pending-changes': GObject.ParamSpec.boolean(
|
|
'pending-changes',
|
|
'Pending changes',
|
|
'Indicates if the current values do not match the configured values',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'start-value': GObject.ParamSpec.int(
|
|
'start-value',
|
|
'Start value',
|
|
'Current start value',
|
|
GObject.ParamFlags.READABLE,
|
|
0, 100,
|
|
0
|
|
),
|
|
'end-value': GObject.ParamSpec.int(
|
|
'end-value',
|
|
'End value',
|
|
'Current end value',
|
|
GObject.ParamFlags.READABLE,
|
|
0, 100,
|
|
100
|
|
),
|
|
'settings': GObject.ParamSpec.object(
|
|
'settings',
|
|
'Settings',
|
|
'Settings',
|
|
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
|
Gio.Settings.$gtype
|
|
),
|
|
},
|
|
Signals: {
|
|
'enable-completed': {
|
|
param_types: [GObject.TYPE_JSOBJECT /* error */]
|
|
},
|
|
'disable-completed': {
|
|
param_types: [GObject.TYPE_JSOBJECT /* error */]
|
|
},
|
|
},
|
|
}, class ThinkPadBattery extends GObject.Object {
|
|
constructor(constructProperties = {}) {
|
|
super(constructProperties);
|
|
|
|
if (!this.name) {
|
|
throw Error('Battery name not defined');
|
|
}
|
|
|
|
// Battery directory
|
|
this._baseDirectoryPath = `${BASE_PATH}/${this.name}`;
|
|
this._baseDirectory = Gio.File.new_for_path(this._baseDirectoryPath);
|
|
|
|
// Set paths
|
|
if (this.environment.checkMinKernelVersion(5, 9)) { // kernel 5.9 and newer
|
|
this._startFilePath = `${BASE_PATH}/${this.name}/${START_FILE_NEW}`;
|
|
this._endFilePath = `${BASE_PATH}/${this.name}/${END_FILE_NEW}`;
|
|
} else if (this.environment.checkMinKernelVersion(4, 17)) { // kernel 4.17 and newer
|
|
this._startFilePath = `${BASE_PATH}/${this.name}/${START_FILE_OLD}`;
|
|
this._endFilePath = `${BASE_PATH}/${this.name}/${END_FILE_OLD}`;
|
|
} else { // Unsupported kernel
|
|
throw Error(`Unsupported kernel version (${this.environment.kernelRelease}). A kernel version greater than or equal to 4.17 is required.`);
|
|
}
|
|
|
|
// Activate the directory monitor
|
|
try {
|
|
this._baseMonitor = this._baseDirectory.monitor_directory(Gio.FileMonitorFlags.NONE, null);
|
|
this._monitorId = this._baseMonitor.connect(
|
|
'changed', (obj, file, otherFile, eventType) => {
|
|
const filePath = file.get_path();
|
|
switch (eventType) {
|
|
case Gio.FileMonitorEvent.CHANGES_DONE_HINT:
|
|
case Gio.FileMonitorEvent.CREATED:
|
|
case Gio.FileMonitorEvent.DELETED:
|
|
switch (filePath) {
|
|
case this._startFilePath:
|
|
this.startValue = readFileInt(this._startFilePath);
|
|
break;
|
|
case this._endFilePath:
|
|
this.endValue = readFileInt(this._endFilePath);
|
|
break;
|
|
case this._baseDirectoryPath:
|
|
this.startValue = readFileInt(this._startFilePath);
|
|
this.endValue = readFileInt(this._endFilePath);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case Gio.FileMonitorEvent.ATTRIBUTE_CHANGED:
|
|
this.authRequired = this._checkAuthRequired();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
);
|
|
} catch (e) {
|
|
logError(e, this.name);
|
|
}
|
|
|
|
this._notifyHandlerId = this.connect('notify', () => this._updateStatuses());
|
|
|
|
this._settingsHandlerId = this.settings.connect('changed', () => this._updateStatuses());
|
|
|
|
// Load initial values
|
|
this.startValue = readFileInt(this._startFilePath);
|
|
this.endValue = readFileInt(this._endFilePath);
|
|
this.authRequired = this._checkAuthRequired();
|
|
}
|
|
|
|
/**
|
|
* Update the statuses
|
|
*/
|
|
_updateStatuses() {
|
|
// Check if is available
|
|
this.isAvailable = this.startValue !== null || this.endValue !== null;
|
|
|
|
// Check if thresholds is active
|
|
const disabledStartValue = this.settings.get_int(`disabled-start-${this.name.toLowerCase()}-value`);
|
|
if (disabledStartValue === -1) {
|
|
// Use model detection
|
|
if (this.environment.productVersion.search(ALTERNATIVE_DISABLED_START) >= 0) {
|
|
this.isActive = (this.startValue !== null && this.startValue !== 95) || (this.endValue !== null && this.endValue !== 100);
|
|
} else {
|
|
this.isActive = (this.startValue !== null && this.startValue !== 0) || (this.endValue !== null && this.endValue !== 100);
|
|
}
|
|
} else {
|
|
// Use user settings
|
|
this.isActive = (this.startValue !== null && this.startValue !== disabledStartValue) || (this.endValue !== null && this.endValue !== 100);
|
|
}
|
|
|
|
const startSetting = this.settings.get_int(`start-${this.name.toLowerCase()}`);
|
|
const endSetting = this.settings.get_int(`end-${this.name.toLowerCase()}`);
|
|
if (this.isActive) {
|
|
this.pendingChanges = (this.startValue !== null && this.startValue !== startSetting) || (this.endValue !== null && this.endValue !== endSetting);
|
|
} else {
|
|
this.pendingChanges = false;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Check if authentication is required to apply the changes
|
|
*
|
|
* @returns {boolean} Returns true if authentication is required to apply the changes
|
|
*/
|
|
_checkAuthRequired() {
|
|
try {
|
|
const f = Gio.File.new_for_path(this._startFilePath);
|
|
const info = f.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
|
|
if (!info.get_attribute_boolean('access::can-write')) {
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
// Ignored
|
|
}
|
|
|
|
try {
|
|
const f = Gio.File.new_for_path(this._endFilePath);
|
|
const info = f.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
|
|
if (!info.get_attribute_boolean('access::can-write')) {
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
// Ignored
|
|
}
|
|
return false;
|
|
}
|
|
|
|
get authRequired() {
|
|
return this._authRequired;
|
|
}
|
|
|
|
set authRequired(value) {
|
|
if (this.authRequired !== value) {
|
|
this._authRequired = value;
|
|
this.notify('auth-required');
|
|
}
|
|
}
|
|
|
|
get isActive() {
|
|
return this._isActive;
|
|
}
|
|
|
|
set isActive(value) {
|
|
if (this.isActive !== value) {
|
|
this._isActive = value;
|
|
this.notify('is-active');
|
|
}
|
|
}
|
|
|
|
get isAvailable() {
|
|
return this._isAvailable;
|
|
}
|
|
|
|
set isAvailable(value) {
|
|
if (this.isAvailable !== value) {
|
|
this._isAvailable = value;
|
|
this.notify('is-available');
|
|
}
|
|
}
|
|
|
|
get startValue() {
|
|
return this._startValue;
|
|
}
|
|
|
|
set startValue(value) {
|
|
if (this.startValue !== value) {
|
|
this._startValue = value;
|
|
this.notify('start-value');
|
|
}
|
|
}
|
|
|
|
get endValue() {
|
|
return this._endValue;
|
|
}
|
|
|
|
set endValue(value) {
|
|
if (this.endValue !== value) {
|
|
this._endValue = value;
|
|
this.notify('end-value');
|
|
}
|
|
}
|
|
|
|
get pendingChanges() {
|
|
return this._pendingChanges;
|
|
}
|
|
|
|
set pendingChanges(value) {
|
|
if (this.pendingChanges !== value) {
|
|
this._pendingChanges = value;
|
|
this.notify('pending-changes');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the command to set the thresholds.
|
|
* This function does not run the command, it just returns the string corresponding to the command.
|
|
* Passed values are not validated by the function, it is your responsibility to validate them first.
|
|
*
|
|
* @param {number} start Start value
|
|
* @param {number} end End value
|
|
* @returns {string|null} Command to modify the thresholds
|
|
*/
|
|
_getSetterCommand(start, end) {
|
|
if (!this.isAvailable) {
|
|
return null;
|
|
}
|
|
|
|
if (!Number.isInteger(start) || !Number.isInteger(end)) {
|
|
throw TypeError(`Thresholds (${start}/${end}) on battery (${this.name}) are not integer`);
|
|
}
|
|
|
|
if (start < 0 || end > 100 || start >= end) {
|
|
throw RangeError(`Thresholds (${start}/${end}) on battery (${this.name}) out of range`);
|
|
}
|
|
|
|
// Commands
|
|
const setStart = `echo ${start.toString()} > ${this._startFilePath}`;
|
|
const setEnd = `echo ${end.toString()} > ${this._endFilePath}`;
|
|
|
|
let oldStart = this.startValue;
|
|
let oldEnd = this.endValue;
|
|
|
|
if ((oldStart === start || oldStart === null) && (oldEnd === end || oldEnd === null)) {
|
|
// Same thresholds
|
|
return null;
|
|
}
|
|
|
|
if (oldStart >= oldEnd) {
|
|
// Invalid threshold reading, happens on ThinkPad E/L series
|
|
oldStart = null;
|
|
oldEnd = null;
|
|
}
|
|
|
|
let command = null;
|
|
|
|
if (fileExists(this._startFilePath) && fileExists(this._endFilePath)) {
|
|
if (oldStart === start) { // Same start, apply only stop
|
|
command = setEnd;
|
|
} else if (oldEnd === end) { // Same stop, apply only start
|
|
command = setStart;
|
|
} else {
|
|
// Determine sequence
|
|
let startStopSequence = true;
|
|
if (oldEnd != null && start > oldEnd) {
|
|
startStopSequence = false;
|
|
}
|
|
if (startStopSequence) {
|
|
command = setStart + ' && ' + setEnd;
|
|
} else {
|
|
command = setEnd + ' && ' + setStart;
|
|
}
|
|
}
|
|
} else if (fileExists(this._startFilePath)) { // Only start available
|
|
command = setStart;
|
|
} else if (fileExists(this._endFilePath)) { // Only stop available
|
|
command = setEnd;
|
|
}
|
|
|
|
return command;
|
|
}
|
|
|
|
/**
|
|
* Get the command string to enable the configured thresholds
|
|
*
|
|
* @returns {string|null} Enable thresholds command string
|
|
*/
|
|
get enableCommand() {
|
|
const startSetting = this.settings.get_int(`start-${this.name.toLowerCase()}`);
|
|
const endSetting = this.settings.get_int(`end-${this.name.toLowerCase()}`);
|
|
return this._getSetterCommand(startSetting, endSetting);
|
|
}
|
|
|
|
/**
|
|
* Get the command string to disable the thresholds
|
|
*
|
|
* @returns {string|null} Enable thresholds command string
|
|
*/
|
|
get disableCommand() {
|
|
return this._getSetterCommand(0, 100);
|
|
}
|
|
|
|
/**
|
|
* Enable the configured thresholds
|
|
*/
|
|
enable() {
|
|
const command = this.enableCommand;
|
|
if (!command) {
|
|
return;
|
|
}
|
|
const runnable = new Runnable(command, this.authRequired);
|
|
runnable.connect('command-completed', (obj, error) => {
|
|
this.emit('enable-completed', error);
|
|
})
|
|
runnable.run();
|
|
}
|
|
|
|
/**
|
|
* Disable thresholds
|
|
*/
|
|
disable() {
|
|
const command = this.disableCommand;
|
|
if (!command) {
|
|
return;
|
|
}
|
|
const runnable = new Runnable(command, this.authRequired);
|
|
runnable.connect('command-completed', (obj, error) => {
|
|
this.emit('disable-completed', error);
|
|
})
|
|
runnable.run();
|
|
}
|
|
|
|
/**
|
|
* Toggle thresholds status
|
|
*/
|
|
toggle() {
|
|
this.isActive ? this.disable() : this.enable();
|
|
}
|
|
|
|
destroy() {
|
|
if (this._notifyHandlerId) {
|
|
this.disconnect(this._notifyHandlerId)
|
|
}
|
|
if (this._settingsHandlerId) {
|
|
this.settings.disconnect(this._settingsHandlerId)
|
|
}
|
|
this._baseMonitor.cancel();
|
|
this._baseMonitor = null;
|
|
this._baseDirectory = null;
|
|
}
|
|
});
|
|
|
|
export const ThinkPad = GObject.registerClass({
|
|
GTypeName: 'ThinkPad',
|
|
Properties: {
|
|
'environment': GObject.ParamSpec.object(
|
|
'environment',
|
|
'Environment',
|
|
'Environment',
|
|
GObject.ParamFlags.READABLE,
|
|
Environment.$gtype
|
|
),
|
|
'is-active': GObject.ParamSpec.boolean(
|
|
'is-active',
|
|
'Is active',
|
|
'Indicates if the thresholds are active or not',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'is-available': GObject.ParamSpec.boolean(
|
|
'is-available',
|
|
'Is available',
|
|
'Indicates if the battery are available or not',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'pending-changes': GObject.ParamSpec.boolean(
|
|
'pending-changes',
|
|
'Pending changes',
|
|
'Indicates if the current values do not match the configured values',
|
|
GObject.ParamFlags.READABLE,
|
|
false
|
|
),
|
|
'batteries': GObject.ParamSpec.jsobject(
|
|
'batteries',
|
|
'Batteries',
|
|
'Batteries object',
|
|
GObject.ParamFlags.READWRITE,
|
|
[]
|
|
),
|
|
'settings': GObject.ParamSpec.object(
|
|
'settings',
|
|
'Settings',
|
|
'Settings',
|
|
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
|
|
Gio.Settings.$gtype
|
|
),
|
|
},
|
|
Signals: {
|
|
'enable-battery-completed': {
|
|
param_types: [GObject.TYPE_OBJECT /* battery */, GObject.TYPE_JSOBJECT /* error */]
|
|
},
|
|
'disable-battery-completed': {
|
|
param_types: [GObject.TYPE_OBJECT /* battery */, GObject.TYPE_JSOBJECT /* error */]
|
|
},
|
|
'enable-all-completed': {
|
|
param_types: [GObject.TYPE_JSOBJECT /* error */]
|
|
},
|
|
'disable-all-completed': {
|
|
param_types: [GObject.TYPE_JSOBJECT /* error */]
|
|
},
|
|
},
|
|
}, class ThinkPad extends GObject.Object {
|
|
constructor(constructProperties = {}) {
|
|
super(constructProperties);
|
|
|
|
if (!this.environment.checkMinKernelVersion(4, 17)) {
|
|
logError(new Error(`Kernel ${this.environment.kernelRelease} not supported`));
|
|
this.batteries = [];
|
|
return;
|
|
}
|
|
|
|
// Signals handlers IDs
|
|
this._batteriesId = {};
|
|
|
|
// Define batteries
|
|
this.batteries = [
|
|
new ThinkPadBattery({
|
|
'environment': this.environment,
|
|
'name': 'BAT0',
|
|
'settings': this.settings
|
|
}),
|
|
new ThinkPadBattery({
|
|
'environment': this.environment,
|
|
'name': 'BAT1',
|
|
'settings': this.settings
|
|
}),
|
|
];
|
|
|
|
// Connect the signals from the batteries to update the flags and notify commands
|
|
this.batteries.forEach(battery => {
|
|
const notifyHandlerId = battery.connect('notify', () => this._updateStatuses());
|
|
|
|
const enableCompletedId = battery.connect(
|
|
'enable-completed', (bat, error) => {
|
|
this.emit('enable-battery-completed', bat, error);
|
|
}
|
|
);
|
|
const disableCompletedId = battery.connect(
|
|
'disable-completed', (bat, error) => {
|
|
this.emit('disable-battery-completed', bat, error);
|
|
}
|
|
);
|
|
this._batteriesId[battery.name] = [notifyHandlerId, enableCompletedId, disableCompletedId];
|
|
});
|
|
|
|
// Load initial values
|
|
this._updateStatuses();
|
|
}
|
|
|
|
get environment() {
|
|
if (this._environment === undefined) {
|
|
this._environment = new Environment();
|
|
}
|
|
return this._environment;
|
|
}
|
|
|
|
get isAvailable() {
|
|
return this._isAvailable;
|
|
}
|
|
|
|
set isAvailable(value) {
|
|
if (this.isAvailable !== value) {
|
|
this._isAvailable = value;
|
|
this.notify('is-available');
|
|
}
|
|
}
|
|
|
|
get isActive() {
|
|
return this._isActive;
|
|
}
|
|
|
|
set isActive(value) {
|
|
if (this.isActive !== value) {
|
|
this._isActive = value;
|
|
this.notify('is-active');
|
|
}
|
|
}
|
|
|
|
get pendingChanges() {
|
|
return this._pendingChanges;
|
|
}
|
|
|
|
set pendingChanges(value) {
|
|
if (this.pendingChanges !== value) {
|
|
this._pendingChanges = value;
|
|
this.notify('pending-changes');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update driver status
|
|
*/
|
|
_updateStatuses() {
|
|
this.isAvailable = this.batteries.some(bat => {
|
|
return bat.isAvailable;
|
|
});
|
|
this.isActive = this.batteries.some(bat => {
|
|
return bat.isActive && bat.isAvailable;
|
|
});
|
|
this.pendingChanges = this.batteries.some(bat => {
|
|
return bat.pendingChanges;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Enable all configured thresholds
|
|
*/
|
|
enableAll() {
|
|
let command = '';
|
|
let authRequired = false;
|
|
this.batteries.forEach(battery => {
|
|
const batteryCommand = battery.enableCommand;
|
|
if (batteryCommand) {
|
|
command = `${command}${command ? ' && ' : ''}${batteryCommand}`;
|
|
authRequired = authRequired || battery.authRequired;
|
|
}
|
|
});
|
|
if (!command) {
|
|
return;
|
|
}
|
|
const runnable = new Runnable(command, authRequired);
|
|
runnable.connect('command-completed', (obj, error) => {
|
|
this.emit('enable-all-completed', error);
|
|
})
|
|
runnable.run();
|
|
}
|
|
|
|
/**
|
|
* Disable all thresholds
|
|
*/
|
|
disableAll() {
|
|
let command = '';
|
|
let authRequired = false;
|
|
this.batteries.forEach(battery => {
|
|
const batteryCommand = battery.disableCommand;
|
|
if (batteryCommand) {
|
|
command = `${command}${command ? ' && ' : ''}${batteryCommand}`;
|
|
authRequired = authRequired || battery.authRequired;
|
|
}
|
|
});
|
|
if (!command) {
|
|
return;
|
|
}
|
|
const runnable = new Runnable(command, authRequired);
|
|
runnable.connect('command-completed', (obj, error) => {
|
|
this.emit('disable-all-completed', error);
|
|
})
|
|
runnable.run();
|
|
}
|
|
|
|
destroy() {
|
|
if (this._batteriesId) {
|
|
this.batteries.forEach(battery => {
|
|
const ids = this._batteriesId[battery.name];
|
|
ids.forEach(id => {
|
|
battery.disconnect(id);
|
|
});
|
|
battery.destroy()
|
|
});
|
|
}
|
|
}
|
|
}); |