├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── python3-thinkpad-tools.spec ├── 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 /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Debian] 28 | - Version: [e.g. 0.9.1-hotfix1] 29 | - Python Version [e.g. 3.8.6 64-bit] 30 | - Thinkpad Model [e.g. T480] 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # Don't upload snap files 29 | *.snap 30 | snap/.snapcraft 31 | prime/ 32 | stage/ 33 | deb_dist/ 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | 113 | # IntelliJ IDEA 114 | .idea 115 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": false, 3 | "python.linting.enabled": true, 4 | "python.linting.pycodestyleEnabled": true 5 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dev@devksingh.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020-2021, Dev Singh 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Update 02/10/2021 2 | 3 | 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! 4 | 5 | ## Update 07/30/2020 6 | 7 | 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. 8 | 9 | Feel free to open PRs with new features or bugfixes! 10 | 11 | --- 12 | # Thinkpad Tools 13 | Tools created to manage thinkpad properties 14 | 15 | ## Currently Supported Properties 16 | * Adjusting Trackpoint Speed and Sensitivity 17 | * Managing battery/batteries 18 | * Setting Charge Stop and Start thresholds 19 | * Checking battery health 20 | * Undervolting CPU (Can write values but cannot read them) 21 | 22 | ## Planned Features 23 | None right now, but feel free to suggest one in issues! 24 | 25 | 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. 26 | 27 | ## Installing Utility 28 | ### Debian/Ubuntu 29 | `.deb` files are available for Debian/Ubuntu on the releases page. 30 | ### Fedora/CentOS 31 | A COPR repository has been created for Fedora/CentOS at `https://copr.fedorainfracloud.org/coprs/dsingh/thinkpad-tools/`. 32 | ### Other distros 33 | Run `python3 setup.py install` after cloning the repository (`git clone https://github.com/devksingh4/thinkpad-tools`). 34 | 35 | ## Supported Devices 36 | 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: 37 | * T480 38 | * X1 Carbon Gen 7 39 | * T470 40 | * X260 41 | 42 | Undervolting is only supported on Skylake or newer Intel CPUs. 43 | 44 | If you have tested this tool to work on more machines, please open a pull request and add it to this list! 45 | 46 | ## Contribution Copyright Assignment 47 | 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. 48 | 49 | ## Persistence of Settings 50 | Run `thinkpad-tools persistence enable` to enable persistence and see the instructions to set the persistent settings. 51 | 52 | 53 | [![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/) 54 | -------------------------------------------------------------------------------- /python3-thinkpad-tools.spec: -------------------------------------------------------------------------------- 1 | %global pypi_name thinkpad-tools 2 | 3 | Name: python-%{pypi_name} 4 | Version: 0.14 5 | Release: 1%{?dist} 6 | Summary: Tools for ThinkPads 7 | 8 | License: GPLv3 9 | URL: https://github.com/devksingh4/thinkpad-tools 10 | Source0: %{pypi_source} 11 | BuildArch: noarch 12 | 13 | BuildRequires: python3-devel 14 | BuildRequires: python3dist(setuptools) 15 | %if 0%{?rhel} < 8 || 0%{?fedora} <= 30 16 | BuildRequires: systemd 17 | %else 18 | BuildRequires: systemd-rpm-macros 19 | %{?systemd_requires} 20 | %endif 21 | 22 | %description 23 | Tools created to manage thinkpad properties such as TrackPoint, Undervolt, and 24 | Battery. 25 | 26 | %package -n %{pypi_name} 27 | Summary: %{summary} 28 | %{?python_provide:%python_provide python3-%{pypi_name}} 29 | 30 | %description -n %{pypi_name} 31 | Tools created to manage thinkpad properties such as TrackPoint, Undervolt, and 32 | Battery 33 | 34 | 35 | %prep 36 | %autosetup -n %{pypi_name}-%{version} 37 | # Remove bundled egg-info 38 | rm -rf %{pypi_name}.egg-info 39 | 40 | %build 41 | %py3_build 42 | 43 | %install 44 | %py3_install 45 | 46 | %files 47 | %doc README.md 48 | %license LICENSE 49 | %{_bindir}/thinkpad-tools 50 | %{python3_sitelib}/thinkpad_tools_assets 51 | %{python3_sitelib}/thinkpad_tools-%{version}-py?.?.egg-info 52 | %config(noreplace) /etc/thinkpad-tools.ini 53 | /usr/lib/systemd/system/thinkpad-tools.service 54 | 55 | %changelog 56 | * Sun April 11 2021 Dev Singh 0.14 57 | - Add ability to read undervolt status 58 | * Tue May 05 2020 Dev Singh 0.13 59 | - Implement true persistence with /etc/thinkpad-tools.ini 60 | * Mon Apr 20 2020 Dev Singh 0.12.2 61 | - Fix error with the TrackPoint script 62 | * Mon Apr 13 2020 Dev Singh 0.12.1 63 | - Comply with Fedora packaging guidelines 64 | * Sun Apr 12 2020 Dev Singh 0.12.0 65 | - Patch documentation strings for persistence mode to show correct options 66 | * Sat Apr 11 2020 Dev Singh 0.11.0 67 | - Initial RPM Release 68 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | 4 | """ 5 | Setup tools wrapper 6 | """ 7 | 8 | from setuptools import find_packages, setup 9 | from shutil import copyfile 10 | 11 | setup( 12 | name='thinkpad-tools', 13 | maintainer="Dev Singh", 14 | maintainer_email="dev@devksingh.com", 15 | version='0.14', 16 | zip_safe=False, 17 | description='Tools for ThinkPads', 18 | long_description="Tools created to manage thinkpad properties such as TrackPoint, Undervolt, and Battery", 19 | platforms=['Linux'], 20 | include_package_data=True, 21 | keywords='thinkpad trackpoint battery undervolt', 22 | packages=find_packages(), 23 | project_urls={ 24 | "Bug Tracker": "https://github.com/devksingh4/thinkpad-tools/issues", 25 | "Documentation": "https://github.com/devksingh4/thinkpad-tools/blob/master/README.md", 26 | "Source Code": "https://github.com/devksingh4/thinkpad-tools/", 27 | }, 28 | license='GPLv3', 29 | scripts=['thinkpad-tools'], 30 | data_files=[ 31 | ('/etc/', ["thinkpad_tools_assets/thinkpad-tools.ini"]), 32 | ('/usr/lib/systemd/system/', ["thinkpad_tools_assets/thinkpad-tools.service"]), 33 | ('/usr/share/licenses/python-thinkpad-tools/', ["LICENSE"]) 34 | 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /stdeb.cfg: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | Suite: focal 3 | XS-Python-Version: >= 3.6 4 | Copyright-File: LICENSE 5 | -------------------------------------------------------------------------------- /thinkpad-tools: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- encoding: utf-8 -*- 3 | 4 | """ 5 | ThinkPad-tools commandline wrapper 6 | """ 7 | import os 8 | import sys 9 | from thinkpad_tools_assets.cmd import commandline_parser 10 | 11 | if __name__ == '__main__': 12 | commandline_parser(sys.argv[1:]) 13 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | 3 | __version__ = '0.13' 4 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/__main__.py: -------------------------------------------------------------------------------- 1 | # __main__.py 2 | 3 | import sys 4 | 5 | from .cmd import commandline_parser 6 | 7 | if __name__ == '__main__': 8 | commandline_parser(sys.argv[1:]) 9 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/battery.py: -------------------------------------------------------------------------------- 1 | """ 2 | Battery related stuff 3 | """ 4 | 5 | 6 | import os 7 | import re 8 | import sys 9 | import pathlib 10 | import argparse 11 | from thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo 12 | 13 | BASE_DIR = pathlib.PurePath('/sys/class/power_supply/') 14 | 15 | PROPERTIES: dict = { 16 | 'alarm': 0, 17 | 'capacity': 100, 'capacity_level': 'Unknown', 18 | 'charge_start_threshold': 0, 'charge_stop_threshold': 100, 19 | 'cycle_count': 0, 20 | 'energy_full': 0, 'energy_full_design': 0, 'energy_now': 0, 21 | 'manufacturer': 'Unknown', 'model_name': 'Unknown', 22 | 'power_now': False, 'present': True, 23 | 'serial_number': 0, 24 | 'status': 'Unknown', 25 | 'technology': 'Unknown', 'type': 'Unknown', 26 | 'voltage_min_design': 0, 'voltage_now': 0 27 | } 28 | 29 | STRING_PROPERTIES: list = [ 30 | 'capacity_level', 31 | 'manufacturer', 'model_name', 32 | 'status', 33 | 'technology', 'type' 34 | ] 35 | 36 | BOOLEAN_PROPERTIES: list = [ 37 | 'power_now', 'present' 38 | ] 39 | 40 | EDITABLE_PROPERTIES: list = [ 41 | 'charge_start_threshold', 'charge_stop_threshold' 42 | ] 43 | 44 | STATUS_STR_TEMPLATE: str = '''\ 45 | Status of battery "{name}": 46 | Alarm: {alarm} Wh 47 | Capacity level: {capacity_level} 48 | Charge start threshold: {charge_start_threshold}% 49 | Charge stop threshold: {charge_stop_threshold}% 50 | Cycle count: {cycle_count} 51 | Current capacity: {energy_full} Wh 52 | Design capacity: {energy_full_design} Wh 53 | Battery health: {battery_health}% 54 | Current energy: {energy_now} Wh 55 | Manufacturer: {manufacturer} 56 | Model name: {model_name} 57 | In use: {power_now} 58 | Present: {present} 59 | Serial number: {serial_number} 60 | Status: {status} 61 | Technology: {technology} 62 | Type: {type} 63 | Minimum design voltage: {voltage_min_design} 64 | Current voltage: {voltage_now}\ 65 | ''' 66 | 67 | USAGE_HEAD: str = '''\ 68 | thinkpad-tools battery [argument] 69 | 70 | Regex can be used in to match multiple batteries 71 | 72 | Supported verbs are: 73 | list List available batteries 74 | status Print all properties 75 | set- Set value 76 | get- Get property 77 | Readable properties: {properties} 78 | Editable properties: {editable_properties} 79 | '''.format( 80 | properties=', '.join(PROPERTIES.keys()), 81 | editable_properties=', '.join(EDITABLE_PROPERTIES) 82 | ) 83 | 84 | USAGE_EXAMPLES: str = '''\ 85 | Examples: 86 | 87 | thinkpad-tools battery list 88 | thinkpad-tools battery set-charge-start-threshold all 80 89 | thinkpad-tools battery set-stop-start-threshold BAT0 90 90 | thinkpad-tools battery get-battery-health 91 | ''' 92 | 93 | 94 | class Battery(object): 95 | """ 96 | Class to handle requests related to Batteries 97 | """ 98 | 99 | def __init__(self, name: str = 'BAT0', **kwargs): 100 | self.name: str = name 101 | self.path: pathlib.PurePath = BASE_DIR / self.name 102 | for prop, default_value in PROPERTIES.items(): 103 | if prop in kwargs.keys(): 104 | if type(kwargs[prop]) == type(default_value): 105 | self.__dict__[prop] = kwargs[prop] 106 | self.__dict__[prop] = default_value 107 | self.battery_health: int = 100 108 | 109 | def read_values(self): 110 | """ 111 | Read values from the system 112 | :return: Nothing 113 | """ 114 | for prop in self.__dict__.keys(): 115 | path = str(self.path / prop) 116 | if os.path.isfile(path): 117 | with open(path) as file: 118 | content = file.readline() 119 | if prop in STRING_PROPERTIES: 120 | self.__dict__[prop] = str(content).strip() 121 | elif prop in BOOLEAN_PROPERTIES: 122 | self.__dict__[prop] = bool(content) 123 | else: 124 | self.__dict__[prop] = int(content) 125 | self.battery_health: int = int( 126 | self.energy_full / self.energy_full_design * 100) 127 | 128 | def set_values(self): 129 | """ 130 | Set values to the system 131 | :return: Nothing 132 | """ 133 | success: bool = True 134 | failures: list = list() 135 | for prop in EDITABLE_PROPERTIES: 136 | if prop not in self.__dict__.keys(): 137 | success = False 138 | failures.append( 139 | 'Property "%s" not found in current object' % prop) 140 | continue 141 | path = str(self.path / prop) 142 | if os.path.isfile(path): 143 | try: 144 | with open(path, 'w') as file: 145 | # TODO: Handle different types of properties 146 | file.write(str(self.__dict__[prop])) 147 | except Exception as e: 148 | success = False 149 | failures.append(str(e)) 150 | if not success: 151 | raise ApplyValueFailedException(', '.join(failures)) 152 | 153 | def get_status_str(self) -> str: 154 | """ 155 | Return status string 156 | :return: str: status string 157 | """ 158 | return STATUS_STR_TEMPLATE.format( 159 | name=str(self.name), 160 | alarm=str(self.alarm / 1000000), 161 | capacity=str(self.capacity), 162 | capacity_level=str(self.capacity_level), 163 | charge_start_threshold=str(self.charge_start_threshold), 164 | charge_stop_threshold=str(self.charge_stop_threshold), 165 | cycle_count=str(self.cycle_count), 166 | energy_full=str(self.energy_full / 1000000), 167 | energy_full_design=str(self.energy_full_design / 1000000), 168 | battery_health=str(self.battery_health), 169 | energy_now=str(self.energy_now / 1000000), 170 | manufacturer=str(self.manufacturer), 171 | model_name=str(self.model_name), 172 | power_now='Yes' if self.power_now else 'No', 173 | present='Yes' if self.present else 'No', 174 | serial_number=str(self.serial_number), 175 | status=str(self.status), 176 | technology=str(self.technology), 177 | type=str(self.type), 178 | voltage_min_design=str(self.voltage_min_design), 179 | voltage_now=str(self.voltage_now) 180 | ) 181 | 182 | 183 | class BatteryHandler(object): 184 | """ 185 | Handler for battery related commands 186 | """ 187 | 188 | def __init__(self): 189 | self.parser: argparse.ArgumentParser = argparse.ArgumentParser( 190 | prog='thinkpad-tools battery', 191 | description='Battery related commands', 192 | usage=USAGE_HEAD, 193 | epilog=USAGE_EXAMPLES, 194 | formatter_class=argparse.RawDescriptionHelpFormatter 195 | ) 196 | self.parser.add_argument( 197 | 'verb', type=str, help='The action going to take') 198 | self.parser.add_argument( 199 | 'battery', nargs='?', type=str, help='The battery') 200 | self.parser.add_argument( 201 | 'arguments', nargs='*', help='Arguments of the action') 202 | self.inner: dict = dict() 203 | for name in os.listdir(str(BASE_DIR)): 204 | if not name.startswith('BAT'): 205 | continue 206 | self.inner[name]: Battery = Battery(name) 207 | 208 | def run(self, unparsed_args: list): 209 | """ 210 | Parse and execute the commands 211 | :param unparsed_args: Unparsed arguments 212 | :return: Nothing 213 | """ 214 | def find_match(battery_name: str) -> list: 215 | """ 216 | Find matched batteries 217 | :param battery_name: name(regex) of the battery 218 | :return: list: List of matched battery/batteries 219 | """ 220 | if battery_name.lower() == 'all': 221 | return list(self.inner.keys()) 222 | try: 223 | pattern: re.Pattern = re.compile(battery_name) 224 | except re.error as e: 225 | print( 226 | 'Invalid matching pattern "%s", %s' % ( 227 | battery_name, str(e)), 228 | file=sys.stderr 229 | ) 230 | exit(1) 231 | return list(filter(pattern.match, self.inner.keys())) 232 | 233 | def invalid_battery(battery_name: str): 234 | """ 235 | No battery found for the given pattern 236 | :param battery_name: pattern of the battery 237 | :return: Nothing, the program exits with status code 1 238 | """ 239 | print( 240 | 'No battery found for pattern"%s", \ 241 | available battery(ies): ' % battery_name + 242 | ', '.join(self.inner.keys()), 243 | file=sys.stderr 244 | ) 245 | exit(1) 246 | 247 | def invalid_property( 248 | prop_name: str, battery_name: str, exit_code: int): 249 | """ 250 | Invalid property 251 | :param prop_name: Name of the property 252 | :param battery_name: Name of the battery 253 | :param exit_code: Exit code going to be used 254 | :return: Nothing, the program exits with the given status code 255 | """ 256 | print( 257 | 'Invalid property "%s", available properties: ' % prop_name + 258 | ', '.join(self.inner[battery_name].__dict__.keys()), 259 | file=sys.stderr 260 | ) 261 | exit(exit_code) 262 | 263 | # Parse arguments 264 | args: argparse.Namespace = self.parser.parse_args(unparsed_args) 265 | verb: str = str(args.verb).lower() 266 | if not args.battery: 267 | battery: str = 'all' 268 | else: 269 | battery: str = str(args.battery) 270 | names: list = find_match(battery) 271 | 272 | # Read values from the system 273 | for name in names: 274 | self.inner[name].read_values() 275 | 276 | # Commands 277 | if verb == 'list': 278 | print(' '.join(self.inner.keys())) 279 | return 280 | 281 | if verb == 'status': 282 | result: list = list() 283 | for name in names: 284 | result.append(self.inner[name].get_status_str()) 285 | if len(result) == 0: 286 | invalid_battery(battery) 287 | print('\n'.join(result)) 288 | return 289 | 290 | if verb.startswith('set-'): 291 | if os.getuid() != 0: 292 | raise NotSudo("Script must be run as superuser/sudo") 293 | try: 294 | prop: str = verb.split('-', maxsplit=1)[1].replace('-', '_') 295 | except IndexError: 296 | print('Invalid command', file=sys.stderr) 297 | exit(1) 298 | for name in names: 299 | if (prop not in EDITABLE_PROPERTIES) or\ 300 | (prop not in self.inner[name].__dict__.keys()): 301 | invalid_property(prop, name, 1) 302 | value: str = ''.join(args.arguments) 303 | if not value: 304 | print('Value is needed', file=sys.stderr) 305 | exit(1) 306 | return 307 | self.inner[name].__dict__[prop] = int(value) 308 | try: 309 | self.inner[name].set_values() 310 | except ApplyValueFailedException as e: 311 | print(str(e), file=sys.stderr) 312 | exit(1) 313 | print(value) 314 | return 315 | 316 | if verb.startswith('get-'): 317 | try: 318 | prop: str = verb.split('-', maxsplit=1)[1].replace('-', '_') 319 | except IndexError: 320 | print('Invalid command', file=sys.stderr) 321 | exit(1) 322 | result: list = list() 323 | for name in names: 324 | if prop not in self.inner[name].__dict__.keys(): 325 | invalid_property(prop, name, 1) 326 | result.append(str(self.inner[name].__dict__[prop])) 327 | print(' '.join(result)) 328 | return 329 | 330 | # No match found 331 | print('Command "%s" not found' % verb, file=sys.stderr) 332 | exit(1) 333 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/classes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import sys 4 | import struct 5 | import subprocess 6 | from struct import pack, unpack 7 | 8 | 9 | class UndervoltSystem(object): 10 | def __init__(self): 11 | pass 12 | 13 | def applyUndervolt(self, mv, plane): 14 | """ 15 | Apply undervolt to system MSR for Intel-based systems 16 | :return: int error: error code to pass 17 | """ 18 | error = 0 19 | uv_value = format( 20 | 0xFFE00000 & ((round(mv*1.024) & 0xFFF) << 21), '08x').upper() 21 | final_val = int(("0x80000" + str(plane) + "11" + uv_value), 16) 22 | n: list = glob.glob('/dev/cpu/[0-9]*/msr') 23 | for c in n: 24 | f: int = os.open(c, os.O_WRONLY) 25 | os.lseek(f, 0x150, os.SEEK_SET) # MSR register 0x150 26 | os.write(f, struct.pack('Q', final_val)) # Write final val 27 | os.close(f) 28 | if not n: 29 | raise OSError("MSR not available. Is Secure Boot Disabled? \ 30 | If not, it must be disabled for this to work.") 31 | return error 32 | 33 | def parseReadUndervolt(self, offset): 34 | plane = int(offset / (1 << 40)) 35 | unpack_val_unround = offset ^ (plane << 40) 36 | temp = unpack_val_unround >> 21 37 | unpack_val = temp if temp <= 1024 else - (2048-temp) 38 | unpack_val_round = unpack_val / 1.024 39 | return f"{str(round(unpack_val_round))}" 40 | 41 | def readUndervolt(self, plane): 42 | """ 43 | Read undervolt offset on given plane 44 | :return: str val: offset on plane in hex 45 | """ 46 | # write read to register for cpu0 47 | final_val = ((1 << 63) | (plane << 40) | (1 << 36) | 0) 48 | f: int = os.open('/dev/cpu/0/msr', os.O_WRONLY) 49 | os.lseek(f, 0x150, os.SEEK_SET) # MSR register 0x150 50 | os.write(f, struct.pack('Q', final_val)) # Write final val 51 | os.close(f) 52 | # now read offset 53 | f: int = os.open('/dev/cpu/0/msr', os.O_RDONLY) 54 | os.lseek(f, 0x150, os.SEEK_SET) 55 | offset, *_ = unpack('Q', os.read(f, 8)) 56 | return self.parseReadUndervolt(offset) 57 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/cmd.py: -------------------------------------------------------------------------------- 1 | # cmd.py 2 | 3 | """ 4 | Commandline parser 5 | """ 6 | 7 | 8 | import logging 9 | import pathlib 10 | import argparse 11 | 12 | # Setup logger 13 | logging.basicConfig(level=logging.INFO) 14 | logger = logging.getLogger(__name__) 15 | 16 | BASE_DIR = pathlib.Path('/etc/thinkpad-tool/') 17 | DEFAULT_CONFIG_PATH = BASE_DIR / 'config.ini' 18 | 19 | USAGE_HEAD = '''\ 20 | thinkpad-tools [] 21 | 22 | Supported properties are: 23 | trackpoint Things related to TrackPoints 24 | battery Things related to batteries 25 | undervolt Things related to undervolting 26 | persistence Things related to editing persistence 27 | ''' 28 | 29 | USAGE_EXAMPLES = '''\ 30 | Examples: 31 | 32 | thinkpad-tools trackpoint status 33 | thinkpad-tools trackpoint set-sensitivity 20 34 | thinkpad-tools battery list 35 | thinkpad-tools battery status all 36 | thinkpad-tools undervolt set-core -20 37 | thinkpad-tools undervolt status 38 | thinkpad-tools persistence edit 39 | ''' 40 | 41 | 42 | def commandline_parser(unparsed_args: None or list = None): 43 | """ 44 | Parse the first argument and call the right handler 45 | :param unparsed_args: Unparsed arguments 46 | :return: Nothing 47 | """ 48 | parser = argparse.ArgumentParser( 49 | prog='thinkpad-tools', 50 | description='Tool for ThinkPads', 51 | usage=USAGE_HEAD, 52 | epilog=USAGE_EXAMPLES, 53 | formatter_class=argparse.RawDescriptionHelpFormatter 54 | ) 55 | parser.add_argument( 56 | 'property', type=str, help='Property going to take action') 57 | prop = str(parser.parse_args(unparsed_args[0:1]).property).lower() 58 | if prop == 'trackpoint': 59 | from .trackpoint import TrackPointHandler 60 | handler = TrackPointHandler() 61 | handler.run(unparsed_args[1:]) 62 | if prop == 'battery': 63 | from .battery import BatteryHandler 64 | handler = BatteryHandler() 65 | handler.run(unparsed_args[1:]) 66 | if prop == 'undervolt': 67 | from .undervolt import UndervoltHandler 68 | handler = UndervoltHandler() 69 | handler.run(unparsed_args[1:]) 70 | if prop == 'persistence': 71 | from .persistence import PersistenceHandler 72 | handler = PersistenceHandler() 73 | handler.run(unparsed_args[1:]) 74 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/persistence.py: -------------------------------------------------------------------------------- 1 | # persistence.py 2 | 3 | """ 4 | Wrapper to edit the persistent settings 5 | """ 6 | 7 | import os 8 | import sys 9 | import pathlib 10 | import argparse 11 | import configparser 12 | import thinkpad_tools_assets.classes 13 | from thinkpad_tools_assets.cmd import commandline_parser 14 | from thinkpad_tools_assets.utils import NotSudo 15 | 16 | try: 17 | if os.getuid() != 0: 18 | raise NotSudo("Script must be run as superuser/sudo") 19 | except NotSudo: 20 | print("ERROR: This script must be run as superuser/sudo") 21 | sys.exit(1) 22 | 23 | USAGE_HEAD: str = '''\ 24 | thinkpad-tools persistence 25 | 26 | Supported verbs are: 27 | edit Edit the persistent settings 28 | enable Enable persistent settings 29 | disable Disable persistent settings 30 | apply Apply the persistent settings 31 | ''' 32 | 33 | USAGE_EXAMPLES: str = '''\ 34 | Examples: 35 | 36 | thinkpad-tools persistence edit 37 | thinkpad-tools persistence disable 38 | thinkpad-tools persistence enable 39 | thinkpad-tools persistence apply 40 | ''' 41 | 42 | 43 | class PersistenceHandler(object): 44 | """ 45 | Handler for Undervolt related commands 46 | """ 47 | def __init__(self): 48 | self.parser: argparse.ArgumentParser = argparse.ArgumentParser( 49 | prog='thinkpad-tools persistence', 50 | description='Edit persistence settings', 51 | usage=USAGE_HEAD, 52 | epilog=USAGE_EXAMPLES, 53 | formatter_class=argparse.RawDescriptionHelpFormatter 54 | ) 55 | self.parser.add_argument('verb', type=str, help='The action going to \ 56 | take') 57 | 58 | def run(self, unparsed_args: list): 59 | """ 60 | Parse and execute the command 61 | :param unparsed_args: Unparsed arguments for this property 62 | :return: Nothing 63 | """ 64 | def invalid_property(prop_name: str, exit_code: int): 65 | """ 66 | Print error message and exit with exit code 1 67 | :param prop_name: Name of the property 68 | :param exit_code: Exit code 69 | :return: Nothing, the problem exits with the given exit code 70 | """ 71 | print( 72 | 'Invalid command "%s", available properties: ' % prop_name + 73 | ', '.join(self.inner.__dict__.keys()), 74 | file=sys.stderr 75 | ) 76 | exit(exit_code) 77 | 78 | # Parse arguments 79 | args: argparse.Namespace = self.parser.parse_args(unparsed_args) 80 | verb: str = str(args.verb).lower() 81 | 82 | # Commands 83 | if verb == 'edit': 84 | try: 85 | editor: str = os.environ['EDITOR'] 86 | except KeyError: 87 | editor: str = "/usr/bin/nano" 88 | os.system('sudo {editor} /etc/thinkpad-tools.ini' 89 | .format(editor=editor)) 90 | return 91 | if verb == "enable": 92 | os.system('systemctl daemon-reload') 93 | os.system('systemctl enable thinkpad-tools.service') 94 | print("""To set persistent settings, please edit the file 95 | '/etc/thinkpad-tools.ini'""") 96 | print("Persistence enabled") 97 | return 98 | if verb == "disable": 99 | os.system('systemctl daemon-reload') 100 | os.system('systemctl disable thinkpad-tools.service') 101 | print("Persistence disabled") 102 | return 103 | if verb == "apply": 104 | config: configparser.ConfigParser = configparser.ConfigParser() 105 | config.read('/etc/thinkpad-tools.ini') 106 | for section in config.sections(): 107 | for (command, val) in config.items(section): 108 | commandline_parser([section, "set-"+command, val]) 109 | return 110 | 111 | # No match found 112 | print('Command "%s" not found' % verb, file=sys.stderr) 113 | exit(1) 114 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/thinkpad-tools.ini: -------------------------------------------------------------------------------- 1 | # [trackpoint] 2 | # speed = 255 3 | # sensitivity = 255 4 | # [undervolt] 5 | # [battery] -------------------------------------------------------------------------------- /thinkpad_tools_assets/thinkpad-tools.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Thinkpad Tools Persistence Service 3 | After=multi-user.target 4 | 5 | [Service] 6 | Type=oneshot 7 | User=root 8 | Group=root 9 | ExecStart=thinkpad-tools persistence apply 10 | 11 | [Install] 12 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /thinkpad_tools_assets/trackpoint.py: -------------------------------------------------------------------------------- 1 | # trackpoint.py 2 | 3 | """ 4 | Trackpoint related stuff 5 | """ 6 | 7 | from thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo 8 | import os 9 | import sys 10 | import pathlib 11 | import argparse 12 | 13 | try: 14 | if os.getuid() != 0: 15 | raise NotSudo("Script must be run as superuser/sudo") 16 | except NotSudo: 17 | print("ERROR: This script must be run as superuser/sudo") 18 | sys.exit(1) 19 | 20 | if os.path.exists("/sys/devices/rmi4-00/rmi4-00.fn03/serio2"): 21 | BASE_PATH = pathlib.PurePath('/sys/devices/rmi4-00/rmi4-00.fn03/serio2') 22 | elif os.path.exists("/sys/devices/rmi4-00/rmi4-00.fn03/serio3"): 23 | BASE_PATH = pathlib.PurePath('/sys/devices/rmi4-00/rmi4-00.fn03/serio3') 24 | else: 25 | BASE_PATH = pathlib.PurePath('/sys/devices/platform/i8042/serio1/serio2') 26 | 27 | STATUS_TEXT = '''\ 28 | Current status: 29 | Sensitivity: {sensitivity} 30 | Speed: {speed}\ 31 | ''' 32 | 33 | USAGE_HEAD: str = '''\ 34 | thinkpad-tools trackpoint [argument] 35 | 36 | Supported verbs are: 37 | status Print all properties 38 | set- Set value 39 | get- Get property 40 | disable Disable trackpoint 41 | Available properties: sensitivity, speed 42 | ''' 43 | 44 | USAGE_EXAMPLES: str = '''\ 45 | Examples: 46 | 47 | thinkpad-tools trackpoint status 48 | thinkpad-tools trackpoint set-sensitivity 20 49 | thinkpad-tools trackpoint get-speed 50 | thinkpad-tools trackpoint disable 51 | ''' 52 | 53 | 54 | class TrackPoint(object): 55 | """ 56 | Class to handle requests related to TrackPoints 57 | """ 58 | 59 | def __init__( 60 | self, 61 | sensitivity: int or None = None, 62 | speed: int or None = None 63 | ): 64 | self.sensitivity = sensitivity 65 | self.speed = speed 66 | 67 | def read_values(self): 68 | """ 69 | Read values from the system 70 | :return: Nothing 71 | """ 72 | for prop in self.__dict__.keys(): 73 | file_path: str = str(BASE_PATH / prop) 74 | if os.path.isfile(file_path): 75 | with open(file_path) as file: 76 | self.__dict__[prop] = file.readline() 77 | else: 78 | self.__dict__[prop] = None 79 | 80 | def set_values(self): 81 | """ 82 | Set values to the system 83 | :return: Nothing 84 | """ 85 | success: bool = True 86 | failures: list = list() 87 | for prop in self.__dict__.keys(): 88 | file_path: str = str(BASE_PATH / prop) 89 | if os.path.isfile(file_path): 90 | try: 91 | with open(file_path, 'w') as file: 92 | file.write(self.__dict__[prop]) 93 | except Exception as e: 94 | success = False 95 | failures.append(str(e)) 96 | if not success: 97 | raise ApplyValueFailedException(', '.join(failures)) 98 | 99 | def disableTrackpoint(self): 100 | """ 101 | Disable the trackpoint 102 | :return: Nothing 103 | """ 104 | success: bool = True 105 | failures: list = list() 106 | for prop in self.__dict__.keys(): 107 | file_path: str = str(BASE_PATH / prop) 108 | if os.path.isfile(file_path): 109 | try: 110 | with open(file_path, 'w') as file: 111 | file.write('0') 112 | except Exception as e: 113 | success = False 114 | failures.append(str(e)) 115 | if not success: 116 | raise ApplyValueFailedException(', '.join(failures)) 117 | 118 | def get_status_str(self) -> str: 119 | """ 120 | Return status string 121 | :return: str: status string 122 | """ 123 | return STATUS_TEXT.format( 124 | sensitivity=self.sensitivity or 'Unknown', 125 | speed=self.speed or 'Unknown' 126 | ) 127 | 128 | 129 | class TrackPointHandler(object): 130 | """ 131 | Handler for TrackPoint related commands 132 | """ 133 | 134 | def __init__(self): 135 | self.parser: argparse.ArgumentParser = argparse.ArgumentParser( 136 | prog='thinkpad-tools trackpoint', 137 | description='TrackPoint related commands', 138 | usage=USAGE_HEAD, 139 | epilog=USAGE_EXAMPLES, 140 | formatter_class=argparse.RawDescriptionHelpFormatter 141 | ) 142 | self.parser.add_argument( 143 | 'verb', type=str, help='The action going to take') 144 | self.parser.add_argument( 145 | 'arguments', nargs='*', help='Arguments of the action') 146 | self.inner: TrackPoint = TrackPoint() 147 | 148 | def run(self, unparsed_args: list): 149 | """ 150 | Parse and execute the command 151 | :param unparsed_args: Unparsed arguments for this property 152 | :return: Nothing 153 | """ 154 | def invalid_property(prop_name: str, exit_code: int): 155 | """ 156 | Print error message and exit with exit code 1 157 | :param prop_name: Name of the property 158 | :param exit_code: Exit code 159 | :return: Nothing, the problem exits with the given exit code 160 | """ 161 | print( 162 | 'Invalid command "%s", available properties: ' % prop_name + 163 | ', '.join(self.inner.__dict__.keys()), 164 | file=sys.stderr 165 | ) 166 | exit(exit_code) 167 | 168 | # Parse arguments 169 | args: argparse.Namespace = self.parser.parse_args(unparsed_args) 170 | verb: str = str(args.verb).lower() 171 | 172 | # Read values from the system 173 | self.inner.read_values() 174 | 175 | # Commands 176 | if verb == 'status': 177 | print(self.inner.get_status_str()) 178 | return 179 | 180 | if verb.startswith('set-'): 181 | try: 182 | prop: str = verb.split('-', maxsplit=1)[1] 183 | except IndexError: 184 | invalid_property(verb, 1) 185 | return 186 | if prop not in self.inner.__dict__.keys(): 187 | invalid_property(prop, 1) 188 | self.inner.__dict__[prop] = str(''.join(args.arguments)) 189 | self.inner.set_values() 190 | print(self.inner.get_status_str()) 191 | return 192 | 193 | if verb.startswith('get-'): 194 | try: 195 | prop: str = verb.split('-', maxsplit=1)[1] 196 | except IndexError: 197 | invalid_property(verb, 1) 198 | return 199 | if not hasattr(self.inner, prop): 200 | invalid_property(prop, 1) 201 | if not self.inner.__dict__[prop]: 202 | print('Unable to read %s' % prop) 203 | exit(1) 204 | print(self.inner.__dict__[prop]) 205 | return 206 | if verb == 'disable': 207 | self.inner.disableTrackpoint() 208 | print(self.inner.get_status_str()) 209 | return 210 | # No match found 211 | print('Command "%s" not found' % verb, file=sys.stderr) 212 | exit(1) 213 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/undervolt.py: -------------------------------------------------------------------------------- 1 | # undervolt.py 2 | 3 | """ 4 | Undervolt related stuff 5 | """ 6 | 7 | import os 8 | import sys 9 | import pathlib 10 | import argparse 11 | import thinkpad_tools_assets.classes 12 | from thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo 13 | 14 | 15 | try: 16 | if os.getuid() != 0: 17 | raise NotSudo("Script must be run as superuser/sudo") 18 | except NotSudo: 19 | print("ERROR: This script must be run as superuser/sudo") 20 | sys.exit(1) 21 | 22 | # PLANE KEY: 23 | # Plane 0: Core 24 | # Plane 1: GPU 25 | # Plane 2: Cache 26 | # Plane 3: Uncore 27 | # Plane 4: Analogio 28 | 29 | STATUS_TEXT = '''\ 30 | Current status: 31 | Core: {core}\n 32 | GPU: {gpu}\n 33 | Cache: {cache}\n 34 | Uncore: {uncore}\n 35 | Analogio: {analogio}\n 36 | ''' 37 | USAGE_HEAD: str = '''\ 38 | thinkpad-tools undervolt [argument] 39 | 40 | Supported verbs are: 41 | status Print all properties 42 | set- Set value 43 | get- Get property 44 | Available properties: core, gpu, cache, uncore, analogio 45 | ''' 46 | 47 | USAGE_EXAMPLES: str = '''\ 48 | Examples: 49 | 50 | thinkpad-tools trackpoint status 51 | thinkpad-tools trackpoint set-core -20 52 | thinkpad-tools trackpoint get-gpu 53 | ''' 54 | 55 | 56 | class Undervolt(object): 57 | """ 58 | Class to handle requests related to Undervolting 59 | """ 60 | 61 | def __init__( 62 | self, 63 | core: float or None = None, 64 | gpu: float or None = None, 65 | cache: float or None = None, 66 | uncore: float or None = None, 67 | analogio: float or None = None, 68 | ): 69 | # self.__register: str = "0x150" 70 | # self.__undervolt_value: str = "0x80000" 71 | self.core = core 72 | self.gpu = gpu 73 | self.cache = cache 74 | self.uncore = uncore 75 | self.analogio = analogio 76 | 77 | def read_values(self): 78 | """ 79 | Read values from the system 80 | :return: Nothing 81 | """ 82 | success = True 83 | failures: list = list() 84 | system = thinkpad_tools_assets.classes.UndervoltSystem() 85 | for prop in self.__dict__.keys(): 86 | plane_hashmap = {"core": 0, "gpu": 1, "cache": 2, "uncore": 3, "analogio": 4} 87 | h: str = '' 88 | try: 89 | plane = plane_hashmap[prop] 90 | h = system.readUndervolt(plane) 91 | except Exception as e: 92 | success = False 93 | failures.append(str(e)) 94 | self.__dict__[prop] = h 95 | if not success: 96 | raise ApplyValueFailedException(', '.join(failures)) 97 | 98 | def set_values(self): 99 | """ 100 | Set values to the system MSR using undervolt function 101 | :return: Nothing 102 | """ 103 | system = thinkpad_tools_assets.classes.UndervoltSystem() 104 | success: bool = True 105 | failures: list = list() 106 | plane_hashmap = {"core": 0, "gpu": 1, "cache": 2, "uncore": 3, "analogio": 4} 107 | for prop in self.__dict__.keys(): 108 | if self.__dict__[prop] is None: 109 | continue 110 | try: 111 | plane: int = plane_hashmap[prop] 112 | system.applyUndervolt(int(self.__dict__[prop]), plane) 113 | except Exception as e: 114 | success = False 115 | failures.append(str(e)) 116 | if not success: 117 | raise ApplyValueFailedException(', '.join(failures)) 118 | 119 | def get_status_str(self) -> str: 120 | """ 121 | Return status string 122 | :return: str: status string 123 | """ 124 | return STATUS_TEXT.format( 125 | core=self.core, 126 | gpu=self.gpu, 127 | cache=self.cache, 128 | uncore=self.uncore, 129 | analogio=self.analogio 130 | ) 131 | 132 | 133 | class UndervoltHandler(object): 134 | """ 135 | Handler for Undervolt related commands 136 | """ 137 | def __init__(self): 138 | self.parser: argparse.ArgumentParser = argparse.ArgumentParser( 139 | prog='thinkpad-tools undervolt', 140 | description='Undervolt related commands', 141 | usage=USAGE_HEAD, 142 | epilog=USAGE_EXAMPLES, 143 | formatter_class=argparse.RawDescriptionHelpFormatter 144 | ) 145 | self.parser.add_argument('verb', type=str, help='The action going to \ 146 | take') 147 | self.parser.add_argument( 148 | 'arguments', nargs='*', help='Arguments of the action') 149 | self.inner: Undervolt = Undervolt() 150 | 151 | def run(self, unparsed_args: list): 152 | """ 153 | Parse and execute the command 154 | :param unparsed_args: Unparsed arguments for this property 155 | :return: Nothing 156 | """ 157 | def invalid_property(prop_name: str, exit_code: int): 158 | """ 159 | Print error message and exit with exit code 1 160 | :param prop_name: Name of the property 161 | :param exit_code: Exit code 162 | :return: Nothing, the problem exits with the given exit code 163 | """ 164 | print( 165 | 'Invalid command "%s", available properties: ' % prop_name + 166 | ', '.join(self.inner.__dict__.keys()), 167 | file=sys.stderr 168 | ) 169 | exit(exit_code) 170 | 171 | # Parse arguments 172 | args: argparse.Namespace = self.parser.parse_args(unparsed_args) 173 | verb: str = str(args.verb).lower() 174 | 175 | # Read values from the system 176 | self.inner.read_values() 177 | 178 | # Commands 179 | if verb == 'status': 180 | print(self.inner.get_status_str()) 181 | return 182 | 183 | if verb.startswith('set-'): 184 | try: 185 | prop: str = verb.split('-', maxsplit=1)[1] 186 | except IndexError: 187 | invalid_property(verb, 1) 188 | return 189 | if prop not in self.inner.__dict__.keys(): 190 | invalid_property(prop, 1) 191 | self.inner.__dict__[prop] = str(''.join(args.arguments)) 192 | self.inner.set_values() 193 | print(self.inner.get_status_str()) 194 | return 195 | 196 | if verb.startswith('get-'): 197 | try: 198 | prop: str = verb.split('-', maxsplit=1)[1] 199 | except IndexError: 200 | invalid_property(verb, 1) 201 | if not hasattr(self.inner, prop): 202 | invalid_property(prop, 1) 203 | if not self.inner.__dict__[prop]: 204 | print('Unable to read %s' % prop) 205 | exit(1) 206 | print(self.inner.__dict__[prop]) 207 | return 208 | 209 | # No match found 210 | print('Command "%s" not found' % verb, file=sys.stderr) 211 | exit(1) 212 | -------------------------------------------------------------------------------- /thinkpad_tools_assets/utils.py: -------------------------------------------------------------------------------- 1 | # thinkpad_tools_assets.utils.py 2 | 3 | 4 | class ApplyValueFailedException(Exception): 5 | """ 6 | Exception raised when failed to apply settings 7 | """ 8 | pass 9 | 10 | 11 | class NotSudo(Exception): 12 | pass 13 | -------------------------------------------------------------------------------- /upload-pypi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo rm -rf dist && sudo python3 setup.py sdist && twine upload dist/* --------------------------------------------------------------------------------