Repository: devksingh4/thinkpad-tools Branch: master Commit: 437585f8fbff Files: 23 Total size: 43.4 KB Directory structure: gitextract_l20ul__j/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .vscode/ │ └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── requirements.txt ├── setup.py ├── stdeb.cfg ├── thinkpad-tools ├── thinkpad_tools_assets/ │ ├── __init__.py │ ├── __main__.py │ ├── battery.py │ ├── classes.py │ ├── cmd.py │ ├── persistence.py │ ├── thinkpad-tools.ini │ ├── thinkpad-tools.service │ ├── trackpoint.py │ ├── undervolt.py │ └── utils.py └── upload-pypi.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. Debian] - Version: [e.g. 0.9.1-hotfix1] - Python Version [e.g. 3.8.6 64-bit] - Thinkpad Model [e.g. T480] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # Don't upload snap files *.snap snap/.snapcraft prime/ stage/ deb_dist/ # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # IntelliJ IDEA .idea ================================================ FILE: .vscode/settings.json ================================================ { "python.linting.pylintEnabled": false, "python.linting.enabled": true, "python.linting.pycodestyleEnabled": true } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dev@devksingh.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE ================================================ BSD 3-Clause License Copyright (c) 2020-2021, Dev Singh All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ ## Update 02/10/2021 There also exists a somewhat-functioning GUI for this utility, which relies on this utility being installed. You may find it [here](https://github.com/devksingh4/thinkpad-tools-gui). Beware questionable design choices, I am *definitely* not a frontend person! ## Update 07/30/2020 My primary machine is now not a ThinkPad anymore, but rather a desktop computer. I still have my ThinkPad and use it frequently, but not much development is occuring on it. As a result, this tool may not recieve many updates other than to fix bugs brought up by others, or ones I notice during my use. Feel free to open PRs with new features or bugfixes! --- # Thinkpad Tools Tools created to manage thinkpad properties ## Currently Supported Properties * Adjusting Trackpoint Speed and Sensitivity * Managing battery/batteries * Setting Charge Stop and Start thresholds * Checking battery health * Undervolting CPU (Can write values but cannot read them) ## Planned Features None right now, but feel free to suggest one in issues! While most of these tools exist seperately, it would be nice to have a first-class linux tool that allows all of the above to be managed all in one place. This is why I started development on thinkpad-tools. ## Installing Utility ### Debian/Ubuntu `.deb` files are available for Debian/Ubuntu on the releases page. ### Fedora/CentOS A COPR repository has been created for Fedora/CentOS at `https://copr.fedorainfracloud.org/coprs/dsingh/thinkpad-tools/`. ### Other distros Run `python3 setup.py install` after cloning the repository (`git clone https://github.com/devksingh4/thinkpad-tools`). ## Supported Devices While this tool should work for any Core-i (xx10 series and onwards) ThinkPad, the following devices have been tested to work with this tool: * T480 * X1 Carbon Gen 7 * T470 * X260 Undervolting is only supported on Skylake or newer Intel CPUs. If you have tested this tool to work on more machines, please open a pull request and add it to this list! ## Contribution Copyright Assignment By contributing to this codebase, you hereby assign copyright in this code to the project, to be licensed under the same terms as the rest of the code. ## Persistence of Settings Run `thinkpad-tools persistence enable` to enable persistence and see the instructions to set the persistent settings. [![Copr build status](https://copr.fedorainfracloud.org/coprs/dsingh/thinkpad-tools/package/python-thinkpad-tools/status_image/last_build.png)](https://copr.fedorainfracloud.org/a/dsingh/thinkpad-tools/package/python-thinkpad-tools/) ================================================ FILE: requirements.txt ================================================ setuptools ================================================ FILE: setup.py ================================================ #!/usr/bin/env python3 # -*- encoding: utf-8 -*- """ Setup tools wrapper """ from setuptools import find_packages, setup from shutil import copyfile setup( name='thinkpad-tools', maintainer="Dev Singh", maintainer_email="dev@devksingh.com", version='0.14', zip_safe=False, description='Tools for ThinkPads', long_description="Tools created to manage thinkpad properties such as TrackPoint, Undervolt, and Battery", platforms=['Linux'], include_package_data=True, keywords='thinkpad trackpoint battery undervolt', packages=find_packages(), project_urls={ "Bug Tracker": "https://github.com/devksingh4/thinkpad-tools/issues", "Documentation": "https://github.com/devksingh4/thinkpad-tools/blob/master/README.md", "Source Code": "https://github.com/devksingh4/thinkpad-tools/", }, license='GPLv3', scripts=['thinkpad-tools'], data_files=[ ('/etc/', ["thinkpad_tools_assets/thinkpad-tools.ini"]), ('/usr/lib/systemd/system/', ["thinkpad_tools_assets/thinkpad-tools.service"]), ('/usr/share/licenses/python-thinkpad-tools/', ["LICENSE"]) ], ) ================================================ FILE: stdeb.cfg ================================================ [DEFAULT] Suite: focal XS-Python-Version: >= 3.6 Copyright-File: LICENSE ================================================ FILE: thinkpad-tools ================================================ #!/usr/bin/env python3 # -*- encoding: utf-8 -*- """ ThinkPad-tools commandline wrapper """ import os import sys from thinkpad_tools_assets.cmd import commandline_parser if __name__ == '__main__': commandline_parser(sys.argv[1:]) ================================================ FILE: thinkpad_tools_assets/__init__.py ================================================ # __init__.py __version__ = '0.13' ================================================ FILE: thinkpad_tools_assets/__main__.py ================================================ # __main__.py import sys from .cmd import commandline_parser if __name__ == '__main__': commandline_parser(sys.argv[1:]) ================================================ FILE: thinkpad_tools_assets/battery.py ================================================ """ Battery related stuff """ import os import re import sys import pathlib import argparse from thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo BASE_DIR = pathlib.PurePath('/sys/class/power_supply/') PROPERTIES: dict = { 'alarm': 0, 'capacity': 100, 'capacity_level': 'Unknown', 'charge_start_threshold': 0, 'charge_stop_threshold': 100, 'cycle_count': 0, 'energy_full': 0, 'energy_full_design': 0, 'energy_now': 0, 'manufacturer': 'Unknown', 'model_name': 'Unknown', 'power_now': False, 'present': True, 'serial_number': 0, 'status': 'Unknown', 'technology': 'Unknown', 'type': 'Unknown', 'voltage_min_design': 0, 'voltage_now': 0 } STRING_PROPERTIES: list = [ 'capacity_level', 'manufacturer', 'model_name', 'status', 'technology', 'type' ] BOOLEAN_PROPERTIES: list = [ 'power_now', 'present' ] EDITABLE_PROPERTIES: list = [ 'charge_start_threshold', 'charge_stop_threshold' ] STATUS_STR_TEMPLATE: str = '''\ Status of battery "{name}": Alarm: {alarm} Wh Capacity level: {capacity_level} Charge start threshold: {charge_start_threshold}% Charge stop threshold: {charge_stop_threshold}% Cycle count: {cycle_count} Current capacity: {energy_full} Wh Design capacity: {energy_full_design} Wh Battery health: {battery_health}% Current energy: {energy_now} Wh Manufacturer: {manufacturer} Model name: {model_name} In use: {power_now} Present: {present} Serial number: {serial_number} Status: {status} Technology: {technology} Type: {type} Minimum design voltage: {voltage_min_design} Current voltage: {voltage_now}\ ''' USAGE_HEAD: str = '''\ thinkpad-tools battery [argument] Regex can be used in to match multiple batteries Supported verbs are: list List available batteries status Print all properties set- Set value get- Get property Readable properties: {properties} Editable properties: {editable_properties} '''.format( properties=', '.join(PROPERTIES.keys()), editable_properties=', '.join(EDITABLE_PROPERTIES) ) USAGE_EXAMPLES: str = '''\ Examples: thinkpad-tools battery list thinkpad-tools battery set-charge-start-threshold all 80 thinkpad-tools battery set-stop-start-threshold BAT0 90 thinkpad-tools battery get-battery-health ''' class Battery(object): """ Class to handle requests related to Batteries """ def __init__(self, name: str = 'BAT0', **kwargs): self.name: str = name self.path: pathlib.PurePath = BASE_DIR / self.name for prop, default_value in PROPERTIES.items(): if prop in kwargs.keys(): if type(kwargs[prop]) == type(default_value): self.__dict__[prop] = kwargs[prop] self.__dict__[prop] = default_value self.battery_health: int = 100 def read_values(self): """ Read values from the system :return: Nothing """ for prop in self.__dict__.keys(): path = str(self.path / prop) if os.path.isfile(path): with open(path) as file: content = file.readline() if prop in STRING_PROPERTIES: self.__dict__[prop] = str(content).strip() elif prop in BOOLEAN_PROPERTIES: self.__dict__[prop] = bool(content) else: self.__dict__[prop] = int(content) self.battery_health: int = int( self.energy_full / self.energy_full_design * 100) def set_values(self): """ Set values to the system :return: Nothing """ success: bool = True failures: list = list() for prop in EDITABLE_PROPERTIES: if prop not in self.__dict__.keys(): success = False failures.append( 'Property "%s" not found in current object' % prop) continue path = str(self.path / prop) if os.path.isfile(path): try: with open(path, 'w') as file: # TODO: Handle different types of properties file.write(str(self.__dict__[prop])) except Exception as e: success = False failures.append(str(e)) if not success: raise ApplyValueFailedException(', '.join(failures)) def get_status_str(self) -> str: """ Return status string :return: str: status string """ return STATUS_STR_TEMPLATE.format( name=str(self.name), alarm=str(self.alarm / 1000000), capacity=str(self.capacity), capacity_level=str(self.capacity_level), charge_start_threshold=str(self.charge_start_threshold), charge_stop_threshold=str(self.charge_stop_threshold), cycle_count=str(self.cycle_count), energy_full=str(self.energy_full / 1000000), energy_full_design=str(self.energy_full_design / 1000000), battery_health=str(self.battery_health), energy_now=str(self.energy_now / 1000000), manufacturer=str(self.manufacturer), model_name=str(self.model_name), power_now='Yes' if self.power_now else 'No', present='Yes' if self.present else 'No', serial_number=str(self.serial_number), status=str(self.status), technology=str(self.technology), type=str(self.type), voltage_min_design=str(self.voltage_min_design), voltage_now=str(self.voltage_now) ) class BatteryHandler(object): """ Handler for battery related commands """ def __init__(self): self.parser: argparse.ArgumentParser = argparse.ArgumentParser( prog='thinkpad-tools battery', description='Battery related commands', usage=USAGE_HEAD, epilog=USAGE_EXAMPLES, formatter_class=argparse.RawDescriptionHelpFormatter ) self.parser.add_argument( 'verb', type=str, help='The action going to take') self.parser.add_argument( 'battery', nargs='?', type=str, help='The battery') self.parser.add_argument( 'arguments', nargs='*', help='Arguments of the action') self.inner: dict = dict() for name in os.listdir(str(BASE_DIR)): if not name.startswith('BAT'): continue self.inner[name]: Battery = Battery(name) def run(self, unparsed_args: list): """ Parse and execute the commands :param unparsed_args: Unparsed arguments :return: Nothing """ def find_match(battery_name: str) -> list: """ Find matched batteries :param battery_name: name(regex) of the battery :return: list: List of matched battery/batteries """ if battery_name.lower() == 'all': return list(self.inner.keys()) try: pattern: re.Pattern = re.compile(battery_name) except re.error as e: print( 'Invalid matching pattern "%s", %s' % ( battery_name, str(e)), file=sys.stderr ) exit(1) return list(filter(pattern.match, self.inner.keys())) def invalid_battery(battery_name: str): """ No battery found for the given pattern :param battery_name: pattern of the battery :return: Nothing, the program exits with status code 1 """ print( 'No battery found for pattern"%s", \ available battery(ies): ' % battery_name + ', '.join(self.inner.keys()), file=sys.stderr ) exit(1) def invalid_property( prop_name: str, battery_name: str, exit_code: int): """ Invalid property :param prop_name: Name of the property :param battery_name: Name of the battery :param exit_code: Exit code going to be used :return: Nothing, the program exits with the given status code """ print( 'Invalid property "%s", available properties: ' % prop_name + ', '.join(self.inner[battery_name].__dict__.keys()), file=sys.stderr ) exit(exit_code) # Parse arguments args: argparse.Namespace = self.parser.parse_args(unparsed_args) verb: str = str(args.verb).lower() if not args.battery: battery: str = 'all' else: battery: str = str(args.battery) names: list = find_match(battery) # Read values from the system for name in names: self.inner[name].read_values() # Commands if verb == 'list': print(' '.join(self.inner.keys())) return if verb == 'status': result: list = list() for name in names: result.append(self.inner[name].get_status_str()) if len(result) == 0: invalid_battery(battery) print('\n'.join(result)) return if verb.startswith('set-'): if os.getuid() != 0: raise NotSudo("Script must be run as superuser/sudo") try: prop: str = verb.split('-', maxsplit=1)[1].replace('-', '_') except IndexError: print('Invalid command', file=sys.stderr) exit(1) for name in names: if (prop not in EDITABLE_PROPERTIES) or\ (prop not in self.inner[name].__dict__.keys()): invalid_property(prop, name, 1) value: str = ''.join(args.arguments) if not value: print('Value is needed', file=sys.stderr) exit(1) return self.inner[name].__dict__[prop] = int(value) try: self.inner[name].set_values() except ApplyValueFailedException as e: print(str(e), file=sys.stderr) exit(1) print(value) return if verb.startswith('get-'): try: prop: str = verb.split('-', maxsplit=1)[1].replace('-', '_') except IndexError: print('Invalid command', file=sys.stderr) exit(1) result: list = list() for name in names: if prop not in self.inner[name].__dict__.keys(): invalid_property(prop, name, 1) result.append(str(self.inner[name].__dict__[prop])) print(' '.join(result)) return # No match found print('Command "%s" not found' % verb, file=sys.stderr) exit(1) ================================================ FILE: thinkpad_tools_assets/classes.py ================================================ import os import glob import sys import struct import subprocess from struct import pack, unpack class UndervoltSystem(object): def __init__(self): pass def applyUndervolt(self, mv, plane): """ Apply undervolt to system MSR for Intel-based systems :return: int error: error code to pass """ error = 0 uv_value = format( 0xFFE00000 & ((round(mv*1.024) & 0xFFF) << 21), '08x').upper() final_val = int(("0x80000" + str(plane) + "11" + uv_value), 16) n: list = glob.glob('/dev/cpu/[0-9]*/msr') for c in n: f: int = os.open(c, os.O_WRONLY) os.lseek(f, 0x150, os.SEEK_SET) # MSR register 0x150 os.write(f, struct.pack('Q', final_val)) # Write final val os.close(f) if not n: raise OSError("MSR not available. Is Secure Boot Disabled? \ If not, it must be disabled for this to work.") return error def parseReadUndervolt(self, offset): plane = int(offset / (1 << 40)) unpack_val_unround = offset ^ (plane << 40) temp = unpack_val_unround >> 21 unpack_val = temp if temp <= 1024 else - (2048-temp) unpack_val_round = unpack_val / 1.024 return f"{str(round(unpack_val_round))}" def readUndervolt(self, plane): """ Read undervolt offset on given plane :return: str val: offset on plane in hex """ # write read to register for cpu0 final_val = ((1 << 63) | (plane << 40) | (1 << 36) | 0) f: int = os.open('/dev/cpu/0/msr', os.O_WRONLY) os.lseek(f, 0x150, os.SEEK_SET) # MSR register 0x150 os.write(f, struct.pack('Q', final_val)) # Write final val os.close(f) # now read offset f: int = os.open('/dev/cpu/0/msr', os.O_RDONLY) os.lseek(f, 0x150, os.SEEK_SET) offset, *_ = unpack('Q', os.read(f, 8)) return self.parseReadUndervolt(offset) ================================================ FILE: thinkpad_tools_assets/cmd.py ================================================ # cmd.py """ Commandline parser """ import logging import pathlib import argparse # Setup logger logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) BASE_DIR = pathlib.Path('/etc/thinkpad-tool/') DEFAULT_CONFIG_PATH = BASE_DIR / 'config.ini' USAGE_HEAD = '''\ thinkpad-tools [] Supported properties are: trackpoint Things related to TrackPoints battery Things related to batteries undervolt Things related to undervolting persistence Things related to editing persistence ''' USAGE_EXAMPLES = '''\ Examples: thinkpad-tools trackpoint status thinkpad-tools trackpoint set-sensitivity 20 thinkpad-tools battery list thinkpad-tools battery status all thinkpad-tools undervolt set-core -20 thinkpad-tools undervolt status thinkpad-tools persistence edit ''' def commandline_parser(unparsed_args: None or list = None): """ Parse the first argument and call the right handler :param unparsed_args: Unparsed arguments :return: Nothing """ parser = argparse.ArgumentParser( prog='thinkpad-tools', description='Tool for ThinkPads', usage=USAGE_HEAD, epilog=USAGE_EXAMPLES, formatter_class=argparse.RawDescriptionHelpFormatter ) parser.add_argument( 'property', type=str, help='Property going to take action') prop = str(parser.parse_args(unparsed_args[0:1]).property).lower() if prop == 'trackpoint': from .trackpoint import TrackPointHandler handler = TrackPointHandler() handler.run(unparsed_args[1:]) if prop == 'battery': from .battery import BatteryHandler handler = BatteryHandler() handler.run(unparsed_args[1:]) if prop == 'undervolt': from .undervolt import UndervoltHandler handler = UndervoltHandler() handler.run(unparsed_args[1:]) if prop == 'persistence': from .persistence import PersistenceHandler handler = PersistenceHandler() handler.run(unparsed_args[1:]) ================================================ FILE: thinkpad_tools_assets/persistence.py ================================================ # persistence.py """ Wrapper to edit the persistent settings """ import os import sys import pathlib import argparse import configparser import thinkpad_tools_assets.classes from thinkpad_tools_assets.cmd import commandline_parser from thinkpad_tools_assets.utils import NotSudo try: if os.getuid() != 0: raise NotSudo("Script must be run as superuser/sudo") except NotSudo: print("ERROR: This script must be run as superuser/sudo") sys.exit(1) USAGE_HEAD: str = '''\ thinkpad-tools persistence Supported verbs are: edit Edit the persistent settings enable Enable persistent settings disable Disable persistent settings apply Apply the persistent settings ''' USAGE_EXAMPLES: str = '''\ Examples: thinkpad-tools persistence edit thinkpad-tools persistence disable thinkpad-tools persistence enable thinkpad-tools persistence apply ''' class PersistenceHandler(object): """ Handler for Undervolt related commands """ def __init__(self): self.parser: argparse.ArgumentParser = argparse.ArgumentParser( prog='thinkpad-tools persistence', description='Edit persistence settings', usage=USAGE_HEAD, epilog=USAGE_EXAMPLES, formatter_class=argparse.RawDescriptionHelpFormatter ) self.parser.add_argument('verb', type=str, help='The action going to \ take') def run(self, unparsed_args: list): """ Parse and execute the command :param unparsed_args: Unparsed arguments for this property :return: Nothing """ def invalid_property(prop_name: str, exit_code: int): """ Print error message and exit with exit code 1 :param prop_name: Name of the property :param exit_code: Exit code :return: Nothing, the problem exits with the given exit code """ print( 'Invalid command "%s", available properties: ' % prop_name + ', '.join(self.inner.__dict__.keys()), file=sys.stderr ) exit(exit_code) # Parse arguments args: argparse.Namespace = self.parser.parse_args(unparsed_args) verb: str = str(args.verb).lower() # Commands if verb == 'edit': try: editor: str = os.environ['EDITOR'] except KeyError: editor: str = "/usr/bin/nano" os.system('sudo {editor} /etc/thinkpad-tools.ini' .format(editor=editor)) return if verb == "enable": os.system('systemctl daemon-reload') os.system('systemctl enable thinkpad-tools.service') print("""To set persistent settings, please edit the file '/etc/thinkpad-tools.ini'""") print("Persistence enabled") return if verb == "disable": os.system('systemctl daemon-reload') os.system('systemctl disable thinkpad-tools.service') print("Persistence disabled") return if verb == "apply": config: configparser.ConfigParser = configparser.ConfigParser() config.read('/etc/thinkpad-tools.ini') for section in config.sections(): for (command, val) in config.items(section): commandline_parser([section, "set-"+command, val]) return # No match found print('Command "%s" not found' % verb, file=sys.stderr) exit(1) ================================================ FILE: thinkpad_tools_assets/thinkpad-tools.ini ================================================ # [trackpoint] # speed = 255 # sensitivity = 255 # [undervolt] # [battery] ================================================ FILE: thinkpad_tools_assets/thinkpad-tools.service ================================================ [Unit] Description=Thinkpad Tools Persistence Service After=multi-user.target [Service] Type=oneshot User=root Group=root ExecStart=thinkpad-tools persistence apply [Install] WantedBy=multi-user.target ================================================ FILE: thinkpad_tools_assets/trackpoint.py ================================================ # trackpoint.py """ Trackpoint related stuff """ from thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo import os import sys import pathlib import argparse try: if os.getuid() != 0: raise NotSudo("Script must be run as superuser/sudo") except NotSudo: print("ERROR: This script must be run as superuser/sudo") sys.exit(1) if os.path.exists("/sys/devices/rmi4-00/rmi4-00.fn03/serio2"): BASE_PATH = pathlib.PurePath('/sys/devices/rmi4-00/rmi4-00.fn03/serio2') elif os.path.exists("/sys/devices/rmi4-00/rmi4-00.fn03/serio3"): BASE_PATH = pathlib.PurePath('/sys/devices/rmi4-00/rmi4-00.fn03/serio3') else: BASE_PATH = pathlib.PurePath('/sys/devices/platform/i8042/serio1/serio2') STATUS_TEXT = '''\ Current status: Sensitivity: {sensitivity} Speed: {speed}\ ''' USAGE_HEAD: str = '''\ thinkpad-tools trackpoint [argument] Supported verbs are: status Print all properties set- Set value get- Get property disable Disable trackpoint Available properties: sensitivity, speed ''' USAGE_EXAMPLES: str = '''\ Examples: thinkpad-tools trackpoint status thinkpad-tools trackpoint set-sensitivity 20 thinkpad-tools trackpoint get-speed thinkpad-tools trackpoint disable ''' class TrackPoint(object): """ Class to handle requests related to TrackPoints """ def __init__( self, sensitivity: int or None = None, speed: int or None = None ): self.sensitivity = sensitivity self.speed = speed def read_values(self): """ Read values from the system :return: Nothing """ for prop in self.__dict__.keys(): file_path: str = str(BASE_PATH / prop) if os.path.isfile(file_path): with open(file_path) as file: self.__dict__[prop] = file.readline() else: self.__dict__[prop] = None def set_values(self): """ Set values to the system :return: Nothing """ success: bool = True failures: list = list() for prop in self.__dict__.keys(): file_path: str = str(BASE_PATH / prop) if os.path.isfile(file_path): try: with open(file_path, 'w') as file: file.write(self.__dict__[prop]) except Exception as e: success = False failures.append(str(e)) if not success: raise ApplyValueFailedException(', '.join(failures)) def disableTrackpoint(self): """ Disable the trackpoint :return: Nothing """ success: bool = True failures: list = list() for prop in self.__dict__.keys(): file_path: str = str(BASE_PATH / prop) if os.path.isfile(file_path): try: with open(file_path, 'w') as file: file.write('0') except Exception as e: success = False failures.append(str(e)) if not success: raise ApplyValueFailedException(', '.join(failures)) def get_status_str(self) -> str: """ Return status string :return: str: status string """ return STATUS_TEXT.format( sensitivity=self.sensitivity or 'Unknown', speed=self.speed or 'Unknown' ) class TrackPointHandler(object): """ Handler for TrackPoint related commands """ def __init__(self): self.parser: argparse.ArgumentParser = argparse.ArgumentParser( prog='thinkpad-tools trackpoint', description='TrackPoint related commands', usage=USAGE_HEAD, epilog=USAGE_EXAMPLES, formatter_class=argparse.RawDescriptionHelpFormatter ) self.parser.add_argument( 'verb', type=str, help='The action going to take') self.parser.add_argument( 'arguments', nargs='*', help='Arguments of the action') self.inner: TrackPoint = TrackPoint() def run(self, unparsed_args: list): """ Parse and execute the command :param unparsed_args: Unparsed arguments for this property :return: Nothing """ def invalid_property(prop_name: str, exit_code: int): """ Print error message and exit with exit code 1 :param prop_name: Name of the property :param exit_code: Exit code :return: Nothing, the problem exits with the given exit code """ print( 'Invalid command "%s", available properties: ' % prop_name + ', '.join(self.inner.__dict__.keys()), file=sys.stderr ) exit(exit_code) # Parse arguments args: argparse.Namespace = self.parser.parse_args(unparsed_args) verb: str = str(args.verb).lower() # Read values from the system self.inner.read_values() # Commands if verb == 'status': print(self.inner.get_status_str()) return if verb.startswith('set-'): try: prop: str = verb.split('-', maxsplit=1)[1] except IndexError: invalid_property(verb, 1) return if prop not in self.inner.__dict__.keys(): invalid_property(prop, 1) self.inner.__dict__[prop] = str(''.join(args.arguments)) self.inner.set_values() print(self.inner.get_status_str()) return if verb.startswith('get-'): try: prop: str = verb.split('-', maxsplit=1)[1] except IndexError: invalid_property(verb, 1) return if not hasattr(self.inner, prop): invalid_property(prop, 1) if not self.inner.__dict__[prop]: print('Unable to read %s' % prop) exit(1) print(self.inner.__dict__[prop]) return if verb == 'disable': self.inner.disableTrackpoint() print(self.inner.get_status_str()) return # No match found print('Command "%s" not found' % verb, file=sys.stderr) exit(1) ================================================ FILE: thinkpad_tools_assets/undervolt.py ================================================ # undervolt.py """ Undervolt related stuff """ import os import sys import pathlib import argparse import thinkpad_tools_assets.classes from thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo try: if os.getuid() != 0: raise NotSudo("Script must be run as superuser/sudo") except NotSudo: print("ERROR: This script must be run as superuser/sudo") sys.exit(1) # PLANE KEY: # Plane 0: Core # Plane 1: GPU # Plane 2: Cache # Plane 3: Uncore # Plane 4: Analogio STATUS_TEXT = '''\ Current status: Core: {core}\n GPU: {gpu}\n Cache: {cache}\n Uncore: {uncore}\n Analogio: {analogio}\n ''' USAGE_HEAD: str = '''\ thinkpad-tools undervolt [argument] Supported verbs are: status Print all properties set- Set value get- Get property Available properties: core, gpu, cache, uncore, analogio ''' USAGE_EXAMPLES: str = '''\ Examples: thinkpad-tools trackpoint status thinkpad-tools trackpoint set-core -20 thinkpad-tools trackpoint get-gpu ''' class Undervolt(object): """ Class to handle requests related to Undervolting """ def __init__( self, core: float or None = None, gpu: float or None = None, cache: float or None = None, uncore: float or None = None, analogio: float or None = None, ): # self.__register: str = "0x150" # self.__undervolt_value: str = "0x80000" self.core = core self.gpu = gpu self.cache = cache self.uncore = uncore self.analogio = analogio def read_values(self): """ Read values from the system :return: Nothing """ success = True failures: list = list() system = thinkpad_tools_assets.classes.UndervoltSystem() for prop in self.__dict__.keys(): plane_hashmap = {"core": 0, "gpu": 1, "cache": 2, "uncore": 3, "analogio": 4} h: str = '' try: plane = plane_hashmap[prop] h = system.readUndervolt(plane) except Exception as e: success = False failures.append(str(e)) self.__dict__[prop] = h if not success: raise ApplyValueFailedException(', '.join(failures)) def set_values(self): """ Set values to the system MSR using undervolt function :return: Nothing """ system = thinkpad_tools_assets.classes.UndervoltSystem() success: bool = True failures: list = list() plane_hashmap = {"core": 0, "gpu": 1, "cache": 2, "uncore": 3, "analogio": 4} for prop in self.__dict__.keys(): if self.__dict__[prop] is None: continue try: plane: int = plane_hashmap[prop] system.applyUndervolt(int(self.__dict__[prop]), plane) except Exception as e: success = False failures.append(str(e)) if not success: raise ApplyValueFailedException(', '.join(failures)) def get_status_str(self) -> str: """ Return status string :return: str: status string """ return STATUS_TEXT.format( core=self.core, gpu=self.gpu, cache=self.cache, uncore=self.uncore, analogio=self.analogio ) class UndervoltHandler(object): """ Handler for Undervolt related commands """ def __init__(self): self.parser: argparse.ArgumentParser = argparse.ArgumentParser( prog='thinkpad-tools undervolt', description='Undervolt related commands', usage=USAGE_HEAD, epilog=USAGE_EXAMPLES, formatter_class=argparse.RawDescriptionHelpFormatter ) self.parser.add_argument('verb', type=str, help='The action going to \ take') self.parser.add_argument( 'arguments', nargs='*', help='Arguments of the action') self.inner: Undervolt = Undervolt() def run(self, unparsed_args: list): """ Parse and execute the command :param unparsed_args: Unparsed arguments for this property :return: Nothing """ def invalid_property(prop_name: str, exit_code: int): """ Print error message and exit with exit code 1 :param prop_name: Name of the property :param exit_code: Exit code :return: Nothing, the problem exits with the given exit code """ print( 'Invalid command "%s", available properties: ' % prop_name + ', '.join(self.inner.__dict__.keys()), file=sys.stderr ) exit(exit_code) # Parse arguments args: argparse.Namespace = self.parser.parse_args(unparsed_args) verb: str = str(args.verb).lower() # Read values from the system self.inner.read_values() # Commands if verb == 'status': print(self.inner.get_status_str()) return if verb.startswith('set-'): try: prop: str = verb.split('-', maxsplit=1)[1] except IndexError: invalid_property(verb, 1) return if prop not in self.inner.__dict__.keys(): invalid_property(prop, 1) self.inner.__dict__[prop] = str(''.join(args.arguments)) self.inner.set_values() print(self.inner.get_status_str()) return if verb.startswith('get-'): try: prop: str = verb.split('-', maxsplit=1)[1] except IndexError: invalid_property(verb, 1) if not hasattr(self.inner, prop): invalid_property(prop, 1) if not self.inner.__dict__[prop]: print('Unable to read %s' % prop) exit(1) print(self.inner.__dict__[prop]) return # No match found print('Command "%s" not found' % verb, file=sys.stderr) exit(1) ================================================ FILE: thinkpad_tools_assets/utils.py ================================================ # thinkpad_tools_assets.utils.py class ApplyValueFailedException(Exception): """ Exception raised when failed to apply settings """ pass class NotSudo(Exception): pass ================================================ FILE: upload-pypi.sh ================================================ #!/bin/bash sudo rm -rf dist && sudo python3 setup.py sdist && twine upload dist/*