driver.js 28 KB

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