driver.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  1. /**
  2. * Driver for handling thresholds in Lenovo ThinkPad series for models since 2011
  3. *
  4. * Based on information from https://linrunner.de/tlp/faq/battery.html
  5. *
  6. * Original sources: https://github.com/linrunner/TLP/blob/main/bat.d/05-thinkpad
  7. */
  8. 'use strict';
  9. import GLib from 'gi://GLib';
  10. import GObject from 'gi://GObject';
  11. import Gio from 'gi://Gio';
  12. // Driver constants
  13. const BASE_PATH = '/sys/class/power_supply';
  14. const START_FILE_OLD = 'charge_start_threshold'; // kernel 4.17 and newer
  15. const END_FILE_OLD = 'charge_stop_threshold'; // kernel 4.17 and newer
  16. const START_FILE_NEW = 'charge_control_start_threshold'; // kernel 5.9 and newer
  17. const END_FILE_NEW = 'charge_control_end_threshold'; // kernel 5.9 and newer
  18. /**
  19. * Read file contents
  20. *
  21. * @param {string} path Path of file
  22. * @returns {string} File contents
  23. */
  24. const readFile = function(path) {
  25. try {
  26. const f = Gio.File.new_for_path(path);
  27. const [, contents,] = f.load_contents(null);
  28. const decoder = new TextDecoder('utf-8');
  29. return decoder.decode(contents);
  30. } catch (e) {
  31. return null;
  32. }
  33. }
  34. /**
  35. * Read integer value from file
  36. *
  37. * @param {string} path Path of file
  38. * @returns {number|null} Return a integer or null
  39. */
  40. const readFileInt = function(path) {
  41. try {
  42. const v = readFile(path);
  43. if (v) {
  44. return parseInt(v);
  45. } else {
  46. return null;
  47. }
  48. } catch (e) {
  49. return null;
  50. }
  51. }
  52. /**
  53. * Test file/direcory exists
  54. *
  55. * @param {string} path File/directory path
  56. * @returns {boolean}
  57. */
  58. const fileExists = function(path) {
  59. try {
  60. const f = Gio.File.new_for_path(path);
  61. return f.query_exists(null);
  62. } catch (e) {
  63. return false;
  64. }
  65. }
  66. /**
  67. * Environment object
  68. */
  69. const Environment = GObject.registerClass({
  70. GTypeName: 'Environment',
  71. }, class Environment extends GObject.Object {
  72. get productVersion() {
  73. if (this._productVersion === undefined) {
  74. const tmp = readFile('/sys/class/dmi/id/product_version');
  75. if (tmp) {
  76. // Remove non-alphanumeric characters
  77. const sanitize = /([^ A-Za-z0-9])+/g;
  78. this._productVersion = tmp.replace(sanitize, '');
  79. } else {
  80. this._productVersion = null;
  81. }
  82. }
  83. return this._productVersion;
  84. }
  85. get kernelRelease() {
  86. if (this._kernelRelease === undefined) {
  87. try {
  88. let proc = Gio.Subprocess.new(
  89. ['uname', '-r'],
  90. Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
  91. );
  92. const [ok, stdout, stderr] = proc.communicate_utf8(null, null);
  93. proc = null;
  94. this._kernelRelease = stdout.trim();
  95. } catch (e) {
  96. logError(e);
  97. }
  98. }
  99. return this._kernelRelease;
  100. }
  101. get kernelMajorVersion() {
  102. if (this._kernelMajorVersion === undefined) {
  103. [this._kernelMajorVersion , this._kernelMinorVersion] = this.kernelRelease.split('.', 2);
  104. }
  105. return this._kernelMajorVersion;
  106. }
  107. get kernelMinorVersion() {
  108. if (this._kernelMinorVersion === undefined) {
  109. [this._kernelMajorVersion , this._kernelMinorVersion] = this.kernelRelease.split('.', 2);
  110. }
  111. return this._kernelMinorVersion;
  112. }
  113. checkMinKernelVersion(major, minor) {
  114. return (
  115. this.kernelMajorVersion > major ||
  116. (this,this.kernelMajorVersion === major && this.kernelMinorVersion >= minor)
  117. );
  118. }
  119. });
  120. /**
  121. * Execute a command and generate events based on its result
  122. *
  123. * @param {string} command Command to execute
  124. * @param {boolean} asRoot True to run with elevated permissions
  125. */
  126. const Runnable = GObject.registerClass({
  127. GTypeName: 'Runnable',
  128. Signals: {
  129. 'command-completed': {
  130. param_types: [GObject.TYPE_JSOBJECT /* erro */]
  131. }
  132. }
  133. }, class Runnable extends GObject.Object {
  134. constructor(command, asRoot = false) {
  135. super();
  136. if (!command) {
  137. throw Error('The command cannot be an empty string');
  138. }
  139. this._command = command;
  140. this._asRoot = asRoot;
  141. }
  142. run() {
  143. const argv = ['sh', '-c', this._command];
  144. if (this._asRoot) {
  145. argv.unshift('pkexec');
  146. }
  147. try {
  148. const [, pid] = GLib.spawn_async(null, argv, null, GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD, null);
  149. GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) => {
  150. try {
  151. GLib.spawn_check_exit_status(status);
  152. this.emit('command-completed', null);
  153. } catch(e) {
  154. if (e.code == 126) {
  155. // Cancelled
  156. } else {
  157. logError(e);
  158. this.emit('command-completed', e);
  159. }
  160. }
  161. GLib.spawn_close_pid(pid);
  162. });
  163. } catch (e) {
  164. logError(e);
  165. this.emit('command-completed', e);
  166. }
  167. }
  168. });
  169. /**
  170. * ThinkPad battery object
  171. */
  172. export const ThinkPadBattery = GObject.registerClass({
  173. GTypeName: 'ThinkPadBattery',
  174. Properties: {
  175. 'environment': GObject.ParamSpec.object(
  176. 'environment',
  177. 'Environment',
  178. 'Environment',
  179. GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
  180. Environment.$gtype
  181. ),
  182. 'name': GObject.ParamSpec.string(
  183. 'name',
  184. 'Name',
  185. 'Battery name',
  186. GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
  187. null
  188. ),
  189. 'auth-required': GObject.ParamSpec.boolean(
  190. 'auth-required',
  191. 'Auth required',
  192. 'Authorization is required to write the values',
  193. GObject.ParamFlags.READABLE,
  194. false
  195. ),
  196. 'is-active': GObject.ParamSpec.boolean(
  197. 'is-active',
  198. 'Is active',
  199. 'Indicates if the thresholds are active or not',
  200. GObject.ParamFlags.READABLE,
  201. false
  202. ),
  203. 'is-available': GObject.ParamSpec.boolean(
  204. 'is-available',
  205. 'Is available',
  206. 'Indicates if the battery are available or not',
  207. GObject.ParamFlags.READABLE,
  208. false
  209. ),
  210. 'pending-changes': GObject.ParamSpec.boolean(
  211. 'pending-changes',
  212. 'Pending changes',
  213. 'Indicates if the current values do not match the configured values',
  214. GObject.ParamFlags.READABLE,
  215. false
  216. ),
  217. 'start-value': GObject.ParamSpec.int(
  218. 'start-value',
  219. 'Start value',
  220. 'Current start value',
  221. GObject.ParamFlags.READABLE,
  222. 0, 100,
  223. 0
  224. ),
  225. 'end-value': GObject.ParamSpec.int(
  226. 'end-value',
  227. 'End value',
  228. 'Current end value',
  229. GObject.ParamFlags.READABLE,
  230. 0, 100,
  231. 100
  232. ),
  233. 'settings': GObject.ParamSpec.object(
  234. 'settings',
  235. 'Settings',
  236. 'Settings',
  237. GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
  238. Gio.Settings.$gtype
  239. ),
  240. },
  241. Signals: {
  242. 'enable-completed': {
  243. param_types: [GObject.TYPE_JSOBJECT /* error */]
  244. },
  245. 'disable-completed': {
  246. param_types: [GObject.TYPE_JSOBJECT /* error */]
  247. },
  248. },
  249. }, class ThinkPadBattery extends GObject.Object {
  250. constructor(constructProperties = {}) {
  251. super(constructProperties);
  252. if (!this.name) {
  253. throw Error('Battery name not defined');
  254. }
  255. // Signals handlers IDs
  256. this._monitorId = null;
  257. this._settingStartId = null;
  258. this._settingEndId = null;
  259. this._startId = null;
  260. this._endId = null;
  261. // Battery directory
  262. this._baseDirectoryPath = `${BASE_PATH}/${this.name}`;
  263. this._baseDirectory = Gio.File.new_for_path(this._baseDirectoryPath);
  264. // Set paths
  265. if (this.environment.checkMinKernelVersion(5, 9)) { // kernel 5.9 and newer
  266. this._startFilePath = `${BASE_PATH}/${this.name}/${START_FILE_NEW}`;
  267. this._endFilePath = `${BASE_PATH}/${this.name}/${END_FILE_NEW}`;
  268. } else if (this.environment.checkMinKernelVersion(4, 17)) { // kernel 4.17 and newer
  269. this._startFilePath = `${BASE_PATH}/${this.name}/${START_FILE_OLD}`;
  270. this._endFilePath = `${BASE_PATH}/${this.name}/${END_FILE_OLD}`;
  271. } else { // Unsupported kernel
  272. throw Error(`Unsupported kernel version (${this.environment.kernelRelease}). A kernel version greater than or equal to 4.17 is required.`);
  273. }
  274. // Activate the directory monitor
  275. try {
  276. this._baseMonitor = this._baseDirectory.monitor_directory(Gio.FileMonitorFlags.NONE, null);
  277. this._monitorId = this._baseMonitor.connect(
  278. 'changed', (obj, file, otherFile, eventType) => {
  279. const filePath = file.get_path();
  280. switch (eventType) {
  281. case Gio.FileMonitorEvent.CHANGES_DONE_HINT:
  282. case Gio.FileMonitorEvent.CREATED:
  283. case Gio.FileMonitorEvent.DELETED:
  284. switch (filePath) {
  285. case this._startFilePath:
  286. this.startValue = readFileInt(this._startFilePath);
  287. break;
  288. case this._endFilePath:
  289. this.endValue = readFileInt(this._endFilePath);
  290. break;
  291. case this._baseDirectoryPath:
  292. this.startValue = readFileInt(this._startFilePath);
  293. this.endValue = readFileInt(this._endFilePath);
  294. break;
  295. default:
  296. break;
  297. }
  298. break;
  299. case Gio.FileMonitorEvent.ATTRIBUTE_CHANGED:
  300. this.authRequired = this._checkAuthRequired();
  301. break;
  302. default:
  303. break;
  304. }
  305. }
  306. );
  307. } catch (e) {
  308. logError(e, this.name);
  309. }
  310. // Update flags on changes in threshold values
  311. this._startId = this.connect(
  312. 'notify::start-value', () => {
  313. this.isActive = this._checkActive();
  314. this.isAvailable = this.startValue !== null || this.endValue !== null;
  315. this.pendingChanges = this._checkPendingChanges();
  316. }
  317. );
  318. this._endId = this.connect(
  319. 'notify::end-value', () => {
  320. this.isActive = this._checkActive();
  321. this.isAvailable = this.startValue !== null || this.endValue !== null;
  322. this.pendingChanges = this._checkPendingChanges();
  323. }
  324. );
  325. // Update pending changes flag on changes to setting values
  326. this._settingStartId = this.settings.connect(
  327. `changed::start-${this.name.toLowerCase()}`, () => {
  328. this.pendingChanges = this._checkPendingChanges();
  329. }
  330. );
  331. this._settingEndId = this.settings.connect(
  332. `changed::end-${this.name.toLowerCase()}`, () => {
  333. this.pendingChanges = this._checkPendingChanges();
  334. }
  335. );
  336. // Load initial values
  337. this.startValue = readFileInt(this._startFilePath);
  338. this.endValue = readFileInt(this._endFilePath);
  339. this.authRequired = this._checkAuthRequired();
  340. }
  341. /**
  342. * Check if thresholds is active
  343. *
  344. * @returns {boolean} Returns true if the battery has active thresholds
  345. */
  346. _checkActive() {
  347. // RegExs
  348. /* On some models the start threshold 0 is not allowed, instead 95 is used (Ex: E14 Gen 3)
  349. * See issue #8: https://gitlab.com/marcosdalvarez/thinkpad-battery-threshold-extension/-/issues/8
  350. */
  351. const disableStart95 = /(Think[pP]ad) (E14 Gen 3)/;
  352. if (this.environment.productVersion.search(disableStart95) >= 0) {
  353. return (this.startValue !== 95 && this.startValue !== null) || (this.endValue !== 100 && this.endValue !== null);
  354. } else {
  355. return (this.startValue !== 0 && this.startValue !== null) || (this.endValue !== 100 && this.endValue !== null);
  356. }
  357. }
  358. /**
  359. * Check if there are changes to the settings that need to be applied
  360. *
  361. * @returns {boolean} Returns true if there are changes to the settings that need to be applied
  362. */
  363. _checkPendingChanges() {
  364. const startSetting = this.settings.get_int(`start-${this.name.toLowerCase()}`);
  365. const endSetting = this.settings.get_int(`end-${this.name.toLowerCase()}`);
  366. return ((this.startValue != null && this.startValue !== startSetting) || (this.endValue != null && this.endValue !== endSetting));
  367. }
  368. /**
  369. * Check if authentication is required to apply the changes
  370. *
  371. * @returns {boolean} Returns true if authentication is required to apply the changes
  372. */
  373. _checkAuthRequired() {
  374. try {
  375. const f = Gio.File.new_for_path(this._startFilePath);
  376. const info = f.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
  377. if (!info.get_attribute_boolean('access::can-write')) {
  378. return true;
  379. }
  380. } catch (e) {
  381. // Ignored
  382. }
  383. try {
  384. const f = Gio.File.new_for_path(this._endFilePath);
  385. const info = f.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
  386. if (!info.get_attribute_boolean('access::can-write')) {
  387. return true;
  388. }
  389. } catch (e) {
  390. // Ignored
  391. }
  392. return false;
  393. }
  394. get authRequired() {
  395. return this._authRequired;
  396. }
  397. set authRequired(value) {
  398. if (this.authRequired !== value) {
  399. this._authRequired = value;
  400. this.notify('auth-required');
  401. }
  402. }
  403. get isActive() {
  404. return this._isActive;
  405. }
  406. set isActive(value) {
  407. if (this.isActive !== value) {
  408. this._isActive = value;
  409. this.notify('is-active');
  410. }
  411. }
  412. get isAvailable() {
  413. return this._isAvaialble;
  414. }
  415. set isAvailable(value) {
  416. if (this.isAvailable !== value) {
  417. this._isAvaialble = value;
  418. this.notify('is-available');
  419. }
  420. }
  421. get startValue() {
  422. return this._startValue;
  423. }
  424. set startValue(value) {
  425. if (this.startValue !== value) {
  426. this._startValue = value;
  427. this.notify('start-value');
  428. }
  429. }
  430. get endValue() {
  431. return this._endValue;
  432. }
  433. set endValue(value) {
  434. if (this.endValue !== value) {
  435. this._endValue = value;
  436. this.notify('end-value');
  437. }
  438. }
  439. get pendingChanges() {
  440. return this._pendingChanges;
  441. }
  442. set pendingChanges(value) {
  443. if (this.pendingChanges !== value) {
  444. this._pendingChanges = value;
  445. this.notify('pending-changes');
  446. }
  447. }
  448. /**
  449. * Returns the command to set the thresholds.
  450. * This function does not run the command, it just returns the string corresponding to the command.
  451. * Passed values are not validated by the function, it is your responsibility to validate them first.
  452. *
  453. * @param {number} start Start value
  454. * @param {number} end End value
  455. * @returns {string|null} Command to modify the thresholds
  456. */
  457. _getSetterCommand(start, end) {
  458. if (!this.isAvailable) {
  459. return null;
  460. }
  461. if (!Number.isInteger(start) || !Number.isInteger(end)) {
  462. throw TypeError(`Thresholds (${start}/${end}) on battery (${this.name}) are not integer`);
  463. }
  464. if (start < 0 || end > 100 || start >= end) {
  465. throw RangeError(`Thresholds (${start}/${end}) on battery (${this.name}) out of range`);
  466. }
  467. // Commands
  468. const setStart = `echo ${start.toString()} > ${this._startFilePath}`;
  469. const setEnd = `echo ${end.toString()} > ${this._endFilePath}`;
  470. let oldStart = this.startValue;
  471. let oldEnd = this.endValue;
  472. if ((oldStart === start || oldStart === null) && (oldEnd === end || oldEnd === null)) {
  473. // Same thresholds
  474. return null;
  475. }
  476. if (oldStart >= oldEnd) {
  477. // Invalid threshold reading, happens on ThinkPad E/L series
  478. oldStart = null;
  479. oldEnd = null;
  480. }
  481. let command = null;
  482. if (fileExists(this._startFilePath) && fileExists(this._endFilePath)) {
  483. if (oldStart === start) { // Same start, apply only stop
  484. command = setEnd;
  485. } else if (oldEnd === end) { // Same stop, apply only start
  486. command = setStart;
  487. } else {
  488. // Determine sequence
  489. let startStopSequence = true;
  490. if (oldEnd != null && start > oldEnd) {
  491. startStopSequence = false;
  492. }
  493. if (startStopSequence) {
  494. command = setStart + ' && ' + setEnd;
  495. } else {
  496. command = setEnd + ' && ' + setStart;
  497. }
  498. }
  499. } else if (fileExists(this._startFilePath)) { // Only start available
  500. command = setStart;
  501. } else if (fileExists(this._endFilePath)) { // Only stop available
  502. command = setEnd;
  503. }
  504. return command;
  505. }
  506. /**
  507. * Get the command string to enable the configured thresholds
  508. *
  509. * @returns {string|null} Enable thresholds command string
  510. */
  511. get enableCommand() {
  512. const startSetting = this.settings.get_int(`start-${this.name.toLowerCase()}`);
  513. const endSetting = this.settings.get_int(`end-${this.name.toLowerCase()}`);
  514. return this._getSetterCommand(startSetting, endSetting);
  515. }
  516. /**
  517. * Get the command string to disable the thresholds
  518. *
  519. * @returns {string|null} Enable thresholds command string
  520. */
  521. get disableCommand() {
  522. return this._getSetterCommand(0, 100);
  523. }
  524. /**
  525. * Enable the configured thresholds
  526. */
  527. enable() {
  528. const command = this.enableCommand;
  529. if (!command) {
  530. return;
  531. }
  532. const runnable = new Runnable(command, this.authRequired);
  533. runnable.connect('command-completed', (obj, error) => {
  534. this.emit('enable-completed', error);
  535. })
  536. runnable.run();
  537. }
  538. /**
  539. * Disable thresholds
  540. */
  541. disable() {
  542. const command = this.disableCommand;
  543. if (!command) {
  544. return;
  545. }
  546. const runnable = new Runnable(command, this.authRequired);
  547. runnable.connect('command-completed', (obj, error) => {
  548. this.emit('disable-completed', error);
  549. })
  550. runnable.run();
  551. }
  552. /**
  553. * Toggle thresholds status
  554. */
  555. toggle() {
  556. this.isActive ? this.disable() : this.enable();
  557. }
  558. destroy() {
  559. if (this._startId) {
  560. this.disconnect(this._startId);
  561. }
  562. if (this._endId) {
  563. this.disconnect(this._endId);
  564. }
  565. if (this._settingStartId) {
  566. this.settings.disconnect(this._settingStartId);
  567. }
  568. if (this._settingEndId) {
  569. this.settings.disconnect(this._settingEndId);
  570. }
  571. if (this._monitorId) {
  572. this._baseMonitor.disconnect(this._monitorId);
  573. }
  574. this._baseMonitor.cancel();
  575. this._baseMonitor = null;
  576. this._baseDirectory = null;
  577. }
  578. });
  579. export const ThinkPad = GObject.registerClass({
  580. GTypeName: 'ThinkPad',
  581. Properties: {
  582. 'environment': GObject.ParamSpec.object(
  583. 'environment',
  584. 'Environment',
  585. 'Environment',
  586. GObject.ParamFlags.READABLE,
  587. Environment.$gtype
  588. ),
  589. 'is-active': GObject.ParamSpec.boolean(
  590. 'is-active',
  591. 'Is active',
  592. 'Indicates if the thresholds are active or not',
  593. GObject.ParamFlags.READABLE,
  594. false
  595. ),
  596. 'is-available': GObject.ParamSpec.boolean(
  597. 'is-available',
  598. 'Is available',
  599. 'Indicates if the battery are available or not',
  600. GObject.ParamFlags.READABLE,
  601. false
  602. ),
  603. 'batteries': GObject.ParamSpec.jsobject(
  604. 'batteries',
  605. 'Batteries',
  606. 'Batteries object',
  607. GObject.ParamFlags.READWRITE,
  608. []
  609. ),
  610. 'settings': GObject.ParamSpec.object(
  611. 'settings',
  612. 'Settings',
  613. 'Settings',
  614. GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
  615. Gio.Settings.$gtype
  616. ),
  617. },
  618. Signals: {
  619. 'enable-battery-completed': {
  620. param_types: [GObject.TYPE_OBJECT /* battery */, GObject.TYPE_JSOBJECT /* error */]
  621. },
  622. 'disable-battery-completed': {
  623. param_types: [GObject.TYPE_OBJECT /* battery */, GObject.TYPE_JSOBJECT /* error */]
  624. },
  625. 'enable-all-completed': {
  626. param_types: [GObject.TYPE_JSOBJECT /* error */]
  627. },
  628. 'disable-all-completed': {
  629. param_types: [GObject.TYPE_JSOBJECT /* error */]
  630. },
  631. },
  632. }, class ThinkPad extends GObject.Object {
  633. constructor(constructProperties = {}) {
  634. super(constructProperties);
  635. if (!this.environment.checkMinKernelVersion(4, 17)) {
  636. logError(new Error(`Kernel ${this.environment.kernelRelease} not supported`));
  637. this.batteries = [];
  638. return;
  639. }
  640. // Signals handlers IDs
  641. this._batteriesId = {};
  642. // Define batteries
  643. this.batteries = [
  644. new ThinkPadBattery({
  645. 'environment': this.environment,
  646. 'name': 'BAT0',
  647. 'settings': this.settings
  648. }),
  649. new ThinkPadBattery({
  650. 'environment': this.environment,
  651. 'name': 'BAT1',
  652. 'settings': this.settings
  653. }),
  654. ];
  655. // Connect the signals from the batteries to update the flags and notify commands
  656. this.batteries.forEach(battery => {
  657. const isAvailableId = battery.connect(
  658. 'notify::is-available', (bat) => {
  659. this.isAvailable = this._checkAvailable();
  660. }
  661. );
  662. const isActiveId = battery.connect(
  663. 'notify::is-active', (bat) => {
  664. this.isActive = this._checkActive();
  665. }
  666. );
  667. const enableCompletedId = battery.connect(
  668. 'enable-completed', (bat, error) => {
  669. this.emit('enable-battery-completed', bat, error);
  670. }
  671. );
  672. const disableCompletedId = battery.connect(
  673. 'disable-completed', (bat, error) => {
  674. this.emit('disable-battery-completed', bat, error);
  675. }
  676. );
  677. this._batteriesId[battery.name] = [isAvailableId, isActiveId, enableCompletedId, disableCompletedId];
  678. });
  679. // Load initial values
  680. this.isAvailable = this._checkAvailable();
  681. this.isActive = this._checkActive();
  682. }
  683. get environment() {
  684. if (this._environment === undefined) {
  685. this._environment = new Environment();
  686. }
  687. return this._environment;
  688. }
  689. get isAvailable() {
  690. return this._isAvaialble;
  691. }
  692. set isAvailable(value) {
  693. if (this.isAvailable !== value) {
  694. this._isAvaialble = value;
  695. this.notify('is-available');
  696. }
  697. }
  698. get isActive() {
  699. return this._isActive;
  700. }
  701. set isActive(value) {
  702. if (this.isActive !== value) {
  703. this._isActive = value;
  704. this.notify('is-active');
  705. }
  706. }
  707. /**
  708. * Check if at least one battery has the thresholds active
  709. *
  710. * @returns {boolean} Returns true if at least one battery has the thresholds active
  711. */
  712. _checkActive() {
  713. return this.batteries.some(bat => {
  714. return bat.isActive && bat.isAvailable;
  715. });
  716. }
  717. /**
  718. * Check if at least one battery is available to apply the thresholds
  719. *
  720. * @returns {boolean} Returns true if at least one battery is available to apply the thresholds
  721. */
  722. _checkAvailable() {
  723. return this.batteries.some(bat => {
  724. return bat.isAvailable;
  725. });
  726. }
  727. /**
  728. * Enable all configured thresholds
  729. */
  730. enableAll() {
  731. let command = '';
  732. let authRequired = false;
  733. this.batteries.forEach(battery => {
  734. const batteryCommand = battery.enableCommand;
  735. if (batteryCommand) {
  736. command = `${command}${command ? ' && ' : ''}${batteryCommand}`;
  737. authRequired = authRequired || battery.authRequired;
  738. }
  739. });
  740. if (!command) {
  741. return;
  742. }
  743. const runnable = new Runnable(command, authRequired);
  744. runnable.connect('command-completed', (obj, error) => {
  745. this.emit('enable-all-completed', error);
  746. })
  747. runnable.run();
  748. }
  749. /**
  750. * Disable all thresholds
  751. */
  752. disableAll() {
  753. let command = '';
  754. let authRequired = false;
  755. this.batteries.forEach(battery => {
  756. const batteryCommand = battery.disableCommand;
  757. if (batteryCommand) {
  758. command = `${command}${command ? ' && ' : ''}${batteryCommand}`;
  759. authRequired = authRequired || battery.authRequired;
  760. }
  761. });
  762. if (!command) {
  763. return;
  764. }
  765. const runnable = new Runnable(command, authRequired);
  766. runnable.connect('command-completed', (obj, error) => {
  767. this.emit('disable-all-completed', error);
  768. })
  769. runnable.run();
  770. }
  771. destroy() {
  772. if (this._batteriesId) {
  773. this.batteries.forEach(battery => {
  774. const ids = this._batteriesId[battery.name];
  775. ids.forEach(id => {
  776. battery.disconnect(id);
  777. });
  778. battery.destroy()
  779. });
  780. }
  781. }
  782. });