driver.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828
  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. this._notifyHandlerId = this.connect('notify', () => this._updateStatuses());
  312. this._settingsHandlerId = this.settings.connect('changed', () => this._updateStatuses());
  313. // Load initial values
  314. this.startValue = readFileInt(this._startFilePath);
  315. this.endValue = readFileInt(this._endFilePath);
  316. this.authRequired = this._checkAuthRequired();
  317. }
  318. /**
  319. * Update the statuses
  320. */
  321. _updateStatuses() {
  322. // Check if is available
  323. this.isAvailable = this.startValue !== null || this.endValue !== null;
  324. // Check if thresholds is active
  325. const disabledStartValue = this.settings.get_int(`disabled-start-${this.name.toLowerCase()}-value`);
  326. if (disabledStartValue === -1) {
  327. // Use model detection
  328. if (this.environment.productVersion.search(ALTERNATIVE_DISABLED_START) >= 0) {
  329. this.isActive = (this.startValue !== null && this.startValue !== 95) || (this.endValue !== null && this.endValue !== 100);
  330. } else {
  331. this.isActive = (this.startValue !== null && this.startValue !== 0) || (this.endValue !== null && this.endValue !== 100);
  332. }
  333. } else {
  334. // Use user settings
  335. this.isActive = (this.startValue !== null && this.startValue !== disabledStartValue) || (this.endValue !== null && this.endValue !== 100);
  336. }
  337. const startSetting = this.settings.get_int(`start-${this.name.toLowerCase()}`);
  338. const endSetting = this.settings.get_int(`end-${this.name.toLowerCase()}`);
  339. if (this.isActive) {
  340. this.pendingChanges = (this.startValue !== null && this.startValue !== startSetting) || (this.endValue !== null && this.endValue !== endSetting);
  341. } else {
  342. this.pendingChanges = false;
  343. }
  344. }
  345. /**
  346. * Check if authentication is required to apply the changes
  347. *
  348. * @returns {boolean} Returns true if authentication is required to apply the changes
  349. */
  350. _checkAuthRequired() {
  351. try {
  352. const f = Gio.File.new_for_path(this._startFilePath);
  353. const info = f.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
  354. if (!info.get_attribute_boolean('access::can-write')) {
  355. return true;
  356. }
  357. } catch (e) {
  358. // Ignored
  359. }
  360. try {
  361. const f = Gio.File.new_for_path(this._endFilePath);
  362. const info = f.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
  363. if (!info.get_attribute_boolean('access::can-write')) {
  364. return true;
  365. }
  366. } catch (e) {
  367. // Ignored
  368. }
  369. return false;
  370. }
  371. get authRequired() {
  372. return this._authRequired;
  373. }
  374. set authRequired(value) {
  375. if (this.authRequired !== value) {
  376. this._authRequired = value;
  377. this.notify('auth-required');
  378. }
  379. }
  380. get isActive() {
  381. return this._isActive;
  382. }
  383. set isActive(value) {
  384. if (this.isActive !== value) {
  385. this._isActive = value;
  386. this.notify('is-active');
  387. }
  388. }
  389. get isAvailable() {
  390. return this._isAvailable;
  391. }
  392. set isAvailable(value) {
  393. if (this.isAvailable !== value) {
  394. this._isAvailable = value;
  395. this.notify('is-available');
  396. }
  397. }
  398. get startValue() {
  399. return this._startValue;
  400. }
  401. set startValue(value) {
  402. if (this.startValue !== value) {
  403. this._startValue = value;
  404. this.notify('start-value');
  405. }
  406. }
  407. get endValue() {
  408. return this._endValue;
  409. }
  410. set endValue(value) {
  411. if (this.endValue !== value) {
  412. this._endValue = value;
  413. this.notify('end-value');
  414. }
  415. }
  416. get pendingChanges() {
  417. return this._pendingChanges;
  418. }
  419. set pendingChanges(value) {
  420. if (this.pendingChanges !== value) {
  421. this._pendingChanges = value;
  422. this.notify('pending-changes');
  423. }
  424. }
  425. /**
  426. * Returns the command to set the thresholds.
  427. * This function does not run the command, it just returns the string corresponding to the command.
  428. * Passed values are not validated by the function, it is your responsibility to validate them first.
  429. *
  430. * @param {number} start Start value
  431. * @param {number} end End value
  432. * @returns {string|null} Command to modify the thresholds
  433. */
  434. _getSetterCommand(start, end) {
  435. if (!this.isAvailable) {
  436. return null;
  437. }
  438. if (!Number.isInteger(start) || !Number.isInteger(end)) {
  439. throw TypeError(`Thresholds (${start}/${end}) on battery (${this.name}) are not integer`);
  440. }
  441. if (start < 0 || end > 100 || start >= end) {
  442. throw RangeError(`Thresholds (${start}/${end}) on battery (${this.name}) out of range`);
  443. }
  444. // Commands
  445. const setStart = `echo ${start.toString()} > ${this._startFilePath}`;
  446. const setEnd = `echo ${end.toString()} > ${this._endFilePath}`;
  447. let oldStart = this.startValue;
  448. let oldEnd = this.endValue;
  449. if ((oldStart === start || oldStart === null) && (oldEnd === end || oldEnd === null)) {
  450. // Same thresholds
  451. return null;
  452. }
  453. if (oldStart >= oldEnd) {
  454. // Invalid threshold reading, happens on ThinkPad E/L series
  455. oldStart = null;
  456. oldEnd = null;
  457. }
  458. let command = null;
  459. if (fileExists(this._startFilePath) && fileExists(this._endFilePath)) {
  460. if (oldStart === start) { // Same start, apply only stop
  461. command = setEnd;
  462. } else if (oldEnd === end) { // Same stop, apply only start
  463. command = setStart;
  464. } else {
  465. // Determine sequence
  466. let startStopSequence = true;
  467. if (oldEnd != null && start > oldEnd) {
  468. startStopSequence = false;
  469. }
  470. if (startStopSequence) {
  471. command = setStart + ' && ' + setEnd;
  472. } else {
  473. command = setEnd + ' && ' + setStart;
  474. }
  475. }
  476. } else if (fileExists(this._startFilePath)) { // Only start available
  477. command = setStart;
  478. } else if (fileExists(this._endFilePath)) { // Only stop available
  479. command = setEnd;
  480. }
  481. return command;
  482. }
  483. /**
  484. * Get the command string to enable the configured thresholds
  485. *
  486. * @returns {string|null} Enable thresholds command string
  487. */
  488. get enableCommand() {
  489. const startSetting = this.settings.get_int(`start-${this.name.toLowerCase()}`);
  490. const endSetting = this.settings.get_int(`end-${this.name.toLowerCase()}`);
  491. return this._getSetterCommand(startSetting, endSetting);
  492. }
  493. /**
  494. * Get the command string to disable the thresholds
  495. *
  496. * @returns {string|null} Enable thresholds command string
  497. */
  498. get disableCommand() {
  499. return this._getSetterCommand(0, 100);
  500. }
  501. /**
  502. * Enable the configured thresholds
  503. */
  504. enable() {
  505. const command = this.enableCommand;
  506. if (!command) {
  507. return;
  508. }
  509. const runnable = new Runnable(command, this.authRequired);
  510. runnable.connect('command-completed', (obj, error) => {
  511. this.emit('enable-completed', error);
  512. })
  513. runnable.run();
  514. }
  515. /**
  516. * Disable thresholds
  517. */
  518. disable() {
  519. const command = this.disableCommand;
  520. if (!command) {
  521. return;
  522. }
  523. const runnable = new Runnable(command, this.authRequired);
  524. runnable.connect('command-completed', (obj, error) => {
  525. this.emit('disable-completed', error);
  526. })
  527. runnable.run();
  528. }
  529. /**
  530. * Toggle thresholds status
  531. */
  532. toggle() {
  533. this.isActive ? this.disable() : this.enable();
  534. }
  535. destroy() {
  536. if (this._notifyHandlerId) {
  537. this.disconnect(this._notifyHandlerId)
  538. }
  539. if (this._settingsHandlerId) {
  540. this.settings.disconnect(this._settingsHandlerId)
  541. }
  542. this._baseMonitor.cancel();
  543. this._baseMonitor = null;
  544. this._baseDirectory = null;
  545. }
  546. });
  547. export const ThinkPad = GObject.registerClass({
  548. GTypeName: 'ThinkPad',
  549. Properties: {
  550. 'environment': GObject.ParamSpec.object(
  551. 'environment',
  552. 'Environment',
  553. 'Environment',
  554. GObject.ParamFlags.READABLE,
  555. Environment.$gtype
  556. ),
  557. 'is-active': GObject.ParamSpec.boolean(
  558. 'is-active',
  559. 'Is active',
  560. 'Indicates if the thresholds are active or not',
  561. GObject.ParamFlags.READABLE,
  562. false
  563. ),
  564. 'is-available': GObject.ParamSpec.boolean(
  565. 'is-available',
  566. 'Is available',
  567. 'Indicates if the battery are available or not',
  568. GObject.ParamFlags.READABLE,
  569. false
  570. ),
  571. 'pending-changes': GObject.ParamSpec.boolean(
  572. 'pending-changes',
  573. 'Pending changes',
  574. 'Indicates if the current values do not match the configured values',
  575. GObject.ParamFlags.READABLE,
  576. false
  577. ),
  578. 'batteries': GObject.ParamSpec.jsobject(
  579. 'batteries',
  580. 'Batteries',
  581. 'Batteries object',
  582. GObject.ParamFlags.READWRITE,
  583. []
  584. ),
  585. 'settings': GObject.ParamSpec.object(
  586. 'settings',
  587. 'Settings',
  588. 'Settings',
  589. GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
  590. Gio.Settings.$gtype
  591. ),
  592. },
  593. Signals: {
  594. 'enable-battery-completed': {
  595. param_types: [GObject.TYPE_OBJECT /* battery */, GObject.TYPE_JSOBJECT /* error */]
  596. },
  597. 'disable-battery-completed': {
  598. param_types: [GObject.TYPE_OBJECT /* battery */, GObject.TYPE_JSOBJECT /* error */]
  599. },
  600. 'enable-all-completed': {
  601. param_types: [GObject.TYPE_JSOBJECT /* error */]
  602. },
  603. 'disable-all-completed': {
  604. param_types: [GObject.TYPE_JSOBJECT /* error */]
  605. },
  606. },
  607. }, class ThinkPad extends GObject.Object {
  608. constructor(constructProperties = {}) {
  609. super(constructProperties);
  610. if (!this.environment.checkMinKernelVersion(4, 17)) {
  611. logError(new Error(`Kernel ${this.environment.kernelRelease} not supported`));
  612. this.batteries = [];
  613. return;
  614. }
  615. // Signals handlers IDs
  616. this._batteriesId = {};
  617. // Define batteries
  618. this.batteries = [
  619. new ThinkPadBattery({
  620. 'environment': this.environment,
  621. 'name': 'BAT0',
  622. 'settings': this.settings
  623. }),
  624. new ThinkPadBattery({
  625. 'environment': this.environment,
  626. 'name': 'BAT1',
  627. 'settings': this.settings
  628. }),
  629. ];
  630. // Connect the signals from the batteries to update the flags and notify commands
  631. this.batteries.forEach(battery => {
  632. const notifyHandlerId = battery.connect('notify', () => this._updateStatuses());
  633. const enableCompletedId = battery.connect(
  634. 'enable-completed', (bat, error) => {
  635. this.emit('enable-battery-completed', bat, error);
  636. }
  637. );
  638. const disableCompletedId = battery.connect(
  639. 'disable-completed', (bat, error) => {
  640. this.emit('disable-battery-completed', bat, error);
  641. }
  642. );
  643. this._batteriesId[battery.name] = [notifyHandlerId, enableCompletedId, disableCompletedId];
  644. });
  645. // Load initial values
  646. this._updateStatuses();
  647. }
  648. get environment() {
  649. if (this._environment === undefined) {
  650. this._environment = new Environment();
  651. }
  652. return this._environment;
  653. }
  654. get isAvailable() {
  655. return this._isAvailable;
  656. }
  657. set isAvailable(value) {
  658. if (this.isAvailable !== value) {
  659. this._isAvailable = value;
  660. this.notify('is-available');
  661. }
  662. }
  663. get isActive() {
  664. return this._isActive;
  665. }
  666. set isActive(value) {
  667. if (this.isActive !== value) {
  668. this._isActive = value;
  669. this.notify('is-active');
  670. }
  671. }
  672. get pendingChanges() {
  673. return this._pendingChanges;
  674. }
  675. set pendingChanges(value) {
  676. if (this.pendingChanges !== value) {
  677. this._pendingChanges = value;
  678. this.notify('pending-changes');
  679. }
  680. }
  681. /**
  682. * Update driver status
  683. */
  684. _updateStatuses() {
  685. this.isAvailable = this.batteries.some(bat => {
  686. return bat.isAvailable;
  687. });
  688. this.isActive = this.batteries.some(bat => {
  689. return bat.isActive && bat.isAvailable;
  690. });
  691. this.pendingChanges = this.batteries.some(bat => {
  692. return bat.pendingChanges;
  693. });
  694. }
  695. /**
  696. * Enable all configured thresholds
  697. */
  698. enableAll() {
  699. let command = '';
  700. let authRequired = false;
  701. this.batteries.forEach(battery => {
  702. const batteryCommand = battery.enableCommand;
  703. if (batteryCommand) {
  704. command = `${command}${command ? ' && ' : ''}${batteryCommand}`;
  705. authRequired = authRequired || battery.authRequired;
  706. }
  707. });
  708. if (!command) {
  709. return;
  710. }
  711. const runnable = new Runnable(command, authRequired);
  712. runnable.connect('command-completed', (obj, error) => {
  713. this.emit('enable-all-completed', error);
  714. })
  715. runnable.run();
  716. }
  717. /**
  718. * Disable all thresholds
  719. */
  720. disableAll() {
  721. let command = '';
  722. let authRequired = false;
  723. this.batteries.forEach(battery => {
  724. const batteryCommand = battery.disableCommand;
  725. if (batteryCommand) {
  726. command = `${command}${command ? ' && ' : ''}${batteryCommand}`;
  727. authRequired = authRequired || battery.authRequired;
  728. }
  729. });
  730. if (!command) {
  731. return;
  732. }
  733. const runnable = new Runnable(command, authRequired);
  734. runnable.connect('command-completed', (obj, error) => {
  735. this.emit('disable-all-completed', error);
  736. })
  737. runnable.run();
  738. }
  739. destroy() {
  740. if (this._batteriesId) {
  741. this.batteries.forEach(battery => {
  742. const ids = this._batteriesId[battery.name];
  743. ids.forEach(id => {
  744. battery.disconnect(id);
  745. });
  746. battery.destroy()
  747. });
  748. }
  749. }
  750. });