[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. Debian]\n - Version: [e.g. 0.9.1-hotfix1]\n - Python Version [e.g. 3.8.6 64-bit]\n - Thinkpad Model [e.g. T480]\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# Don't upload snap files\n*.snap\nsnap/.snapcraft\nprime/\nstage/\ndeb_dist/\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n# IntelliJ IDEA\n.idea\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"python.linting.pylintEnabled\": false,\n    \"python.linting.enabled\": true,\n    \"python.linting.pycodestyleEnabled\": true\n}"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at dev@devksingh.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2020-2021, Dev Singh\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\n3. Neither the name of the copyright holder nor the names of its\n   contributors may be used to endorse or promote products derived from\n   this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "## Update 02/10/2021\n\nThere 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!\n\n## Update 07/30/2020\n\nMy 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. \n\nFeel free to open PRs with new features or bugfixes!\n\n---\n# Thinkpad Tools\nTools created to manage thinkpad properties\n\n## Currently Supported Properties\n* Adjusting Trackpoint Speed and Sensitivity\n* Managing battery/batteries\n  * Setting Charge Stop and Start thresholds\n  * Checking battery health\n* Undervolting CPU (Can write values but cannot read them)\n\n## Planned Features\nNone right now, but feel free to suggest one in issues!\n\nWhile 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. \n\n## Installing Utility\n### Debian/Ubuntu\n`.deb` files are available for Debian/Ubuntu on the releases page.\n### Fedora/CentOS\nA COPR repository has been created for Fedora/CentOS at `https://copr.fedorainfracloud.org/coprs/dsingh/thinkpad-tools/`.\n### Other distros\nRun `python3 setup.py install` after cloning the repository (`git clone https://github.com/devksingh4/thinkpad-tools`). \n\n## Supported Devices\nWhile this tool should work for any Core-i (xx10 series and onwards) ThinkPad, the following devices have been tested to work with this tool: \n* T480\n* X1 Carbon Gen 7\n* T470\n* X260\n\nUndervolting is only supported on Skylake or newer Intel CPUs. \n\nIf you have tested this tool to work on more machines, please open a pull request and add it to this list!\n\n## Contribution Copyright Assignment\nBy 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.\n\n## Persistence of Settings\nRun `thinkpad-tools persistence enable` to enable persistence and see the instructions to set the persistent settings.\n\n\n[![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/)\n"
  },
  {
    "path": "requirements.txt",
    "content": "setuptools\n"
  },
  {
    "path": "setup.py",
    "content": "#!/usr/bin/env python3\n# -*- encoding: utf-8 -*-\n\n\"\"\"\nSetup tools wrapper\n\"\"\"\n\nfrom setuptools import find_packages, setup\nfrom shutil import copyfile\n\nsetup(\n    name='thinkpad-tools',\n    maintainer=\"Dev Singh\",\n    maintainer_email=\"dev@devksingh.com\",\n    version='0.14',\n    zip_safe=False,\n    description='Tools for ThinkPads',\n    long_description=\"Tools created to manage thinkpad properties such as TrackPoint, Undervolt, and Battery\",\n    platforms=['Linux'],\n    include_package_data=True,\n    keywords='thinkpad trackpoint battery undervolt',\n    packages=find_packages(),\n    project_urls={\n        \"Bug Tracker\": \"https://github.com/devksingh4/thinkpad-tools/issues\",\n        \"Documentation\": \"https://github.com/devksingh4/thinkpad-tools/blob/master/README.md\",\n        \"Source Code\": \"https://github.com/devksingh4/thinkpad-tools/\",\n    },\n    license='GPLv3',\n    scripts=['thinkpad-tools'],\n    data_files=[\n        ('/etc/', [\"thinkpad_tools_assets/thinkpad-tools.ini\"]),\n        ('/usr/lib/systemd/system/', [\"thinkpad_tools_assets/thinkpad-tools.service\"]),\n        ('/usr/share/licenses/python-thinkpad-tools/', [\"LICENSE\"])\n\n    ],\n)\n"
  },
  {
    "path": "stdeb.cfg",
    "content": "[DEFAULT]\nSuite: focal\nXS-Python-Version: >= 3.6\nCopyright-File: LICENSE\n"
  },
  {
    "path": "thinkpad-tools",
    "content": "#!/usr/bin/env python3\n# -*- encoding: utf-8 -*-\n\n\"\"\"\nThinkPad-tools commandline wrapper\n\"\"\"\nimport os\nimport sys\nfrom thinkpad_tools_assets.cmd import commandline_parser\n\nif __name__ == '__main__':\n    commandline_parser(sys.argv[1:])\n"
  },
  {
    "path": "thinkpad_tools_assets/__init__.py",
    "content": "# __init__.py\n\n__version__ = '0.13'\n"
  },
  {
    "path": "thinkpad_tools_assets/__main__.py",
    "content": "# __main__.py\n\nimport sys\n\nfrom .cmd import commandline_parser\n\nif __name__ == '__main__':\n    commandline_parser(sys.argv[1:])\n"
  },
  {
    "path": "thinkpad_tools_assets/battery.py",
    "content": "\"\"\"\nBattery related stuff\n\"\"\"\n\n\nimport os\nimport re\nimport sys\nimport pathlib\nimport argparse\nfrom thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo\n\nBASE_DIR = pathlib.PurePath('/sys/class/power_supply/')\n\nPROPERTIES: dict = {\n    'alarm': 0,\n    'capacity': 100, 'capacity_level': 'Unknown',\n    'charge_start_threshold': 0, 'charge_stop_threshold': 100,\n    'cycle_count': 0,\n    'energy_full': 0, 'energy_full_design': 0, 'energy_now': 0,\n    'manufacturer': 'Unknown', 'model_name': 'Unknown',\n    'power_now': False, 'present': True,\n    'serial_number': 0,\n    'status': 'Unknown',\n    'technology': 'Unknown', 'type': 'Unknown',\n    'voltage_min_design': 0, 'voltage_now': 0\n}\n\nSTRING_PROPERTIES: list = [\n    'capacity_level',\n    'manufacturer', 'model_name',\n    'status',\n    'technology', 'type'\n]\n\nBOOLEAN_PROPERTIES: list = [\n    'power_now', 'present'\n]\n\nEDITABLE_PROPERTIES: list = [\n    'charge_start_threshold', 'charge_stop_threshold'\n]\n\nSTATUS_STR_TEMPLATE: str = '''\\\nStatus of battery \"{name}\":\n  Alarm:                    {alarm} Wh\n  Capacity level:           {capacity_level}\n  Charge start threshold:   {charge_start_threshold}%\n  Charge stop threshold:    {charge_stop_threshold}%\n  Cycle count:              {cycle_count}\n  Current capacity:         {energy_full} Wh\n  Design capacity:          {energy_full_design} Wh\n  Battery health:           {battery_health}%\n  Current energy:           {energy_now} Wh\n  Manufacturer:             {manufacturer}\n  Model name:               {model_name}\n  In use:                   {power_now}\n  Present:                  {present}\n  Serial number:            {serial_number}\n  Status:                   {status}\n  Technology:               {technology}\n  Type:                     {type}\n  Minimum design voltage:   {voltage_min_design}\n  Current voltage:          {voltage_now}\\\n'''\n\nUSAGE_HEAD: str = '''\\\nthinkpad-tools battery <verb> <battery> [argument]\n\nRegex can be used in <battery> to match multiple batteries\n\nSupported verbs are:\n    list            List available batteries\n    status          Print all properties\n    set-<property>  Set value\n    get-<property>  Get property\nReadable properties: {properties}\nEditable properties: {editable_properties}\n'''.format(\n    properties=', '.join(PROPERTIES.keys()),\n    editable_properties=', '.join(EDITABLE_PROPERTIES)\n)\n\nUSAGE_EXAMPLES: str = '''\\\nExamples:\n\nthinkpad-tools battery list\nthinkpad-tools battery set-charge-start-threshold all 80\nthinkpad-tools battery set-stop-start-threshold BAT0 90\nthinkpad-tools battery get-battery-health\n'''\n\n\nclass Battery(object):\n    \"\"\"\n    Class to handle requests related to Batteries\n    \"\"\"\n\n    def __init__(self, name: str = 'BAT0', **kwargs):\n        self.name: str = name\n        self.path: pathlib.PurePath = BASE_DIR / self.name\n        for prop, default_value in PROPERTIES.items():\n            if prop in kwargs.keys():\n                if type(kwargs[prop]) == type(default_value):\n                    self.__dict__[prop] = kwargs[prop]\n            self.__dict__[prop] = default_value\n        self.battery_health: int = 100\n\n    def read_values(self):\n        \"\"\"\n        Read values from the system\n        :return: Nothing\n        \"\"\"\n        for prop in self.__dict__.keys():\n            path = str(self.path / prop)\n            if os.path.isfile(path):\n                with open(path) as file:\n                    content = file.readline()\n                if prop in STRING_PROPERTIES:\n                    self.__dict__[prop] = str(content).strip()\n                elif prop in BOOLEAN_PROPERTIES:\n                    self.__dict__[prop] = bool(content)\n                else:\n                    self.__dict__[prop] = int(content)\n        self.battery_health: int = int(\n            self.energy_full / self.energy_full_design * 100)\n\n    def set_values(self):\n        \"\"\"\n        Set values to the system\n        :return: Nothing\n        \"\"\"\n        success: bool = True\n        failures: list = list()\n        for prop in EDITABLE_PROPERTIES:\n            if prop not in self.__dict__.keys():\n                success = False\n                failures.append(\n                    'Property \"%s\" not found in current object' % prop)\n                continue\n            path = str(self.path / prop)\n            if os.path.isfile(path):\n                try:\n                    with open(path, 'w') as file:\n                        # TODO: Handle different types of properties\n                        file.write(str(self.__dict__[prop]))\n                except Exception as e:\n                    success = False\n                    failures.append(str(e))\n        if not success:\n            raise ApplyValueFailedException(', '.join(failures))\n\n    def get_status_str(self) -> str:\n        \"\"\"\n        Return status string\n        :return: str: status string\n        \"\"\"\n        return STATUS_STR_TEMPLATE.format(\n            name=str(self.name),\n            alarm=str(self.alarm / 1000000),\n            capacity=str(self.capacity),\n            capacity_level=str(self.capacity_level),\n            charge_start_threshold=str(self.charge_start_threshold),\n            charge_stop_threshold=str(self.charge_stop_threshold),\n            cycle_count=str(self.cycle_count),\n            energy_full=str(self.energy_full / 1000000),\n            energy_full_design=str(self.energy_full_design / 1000000),\n            battery_health=str(self.battery_health),\n            energy_now=str(self.energy_now / 1000000),\n            manufacturer=str(self.manufacturer),\n            model_name=str(self.model_name),\n            power_now='Yes' if self.power_now else 'No',\n            present='Yes' if self.present else 'No',\n            serial_number=str(self.serial_number),\n            status=str(self.status),\n            technology=str(self.technology),\n            type=str(self.type),\n            voltage_min_design=str(self.voltage_min_design),\n            voltage_now=str(self.voltage_now)\n        )\n\n\nclass BatteryHandler(object):\n    \"\"\"\n    Handler for battery related commands\n    \"\"\"\n\n    def __init__(self):\n        self.parser: argparse.ArgumentParser = argparse.ArgumentParser(\n            prog='thinkpad-tools battery',\n            description='Battery related commands',\n            usage=USAGE_HEAD,\n            epilog=USAGE_EXAMPLES,\n            formatter_class=argparse.RawDescriptionHelpFormatter\n        )\n        self.parser.add_argument(\n            'verb', type=str, help='The action going to take')\n        self.parser.add_argument(\n            'battery', nargs='?', type=str, help='The battery')\n        self.parser.add_argument(\n            'arguments', nargs='*', help='Arguments of the action')\n        self.inner: dict = dict()\n        for name in os.listdir(str(BASE_DIR)):\n            if not name.startswith('BAT'):\n                continue\n            self.inner[name]: Battery = Battery(name)\n\n    def run(self, unparsed_args: list):\n        \"\"\"\n        Parse and execute the commands\n        :param unparsed_args: Unparsed arguments\n        :return: Nothing\n        \"\"\"\n        def find_match(battery_name: str) -> list:\n            \"\"\"\n            Find matched batteries\n            :param battery_name: name(regex) of the battery\n            :return: list: List of matched battery/batteries\n            \"\"\"\n            if battery_name.lower() == 'all':\n                return list(self.inner.keys())\n            try:\n                pattern: re.Pattern = re.compile(battery_name)\n            except re.error as e:\n                print(\n                    'Invalid matching pattern \"%s\", %s' % (\n                        battery_name, str(e)),\n                    file=sys.stderr\n                )\n                exit(1)\n            return list(filter(pattern.match, self.inner.keys()))\n\n        def invalid_battery(battery_name: str):\n            \"\"\"\n            No battery found for the given pattern\n            :param battery_name: pattern of the battery\n            :return: Nothing, the program exits with status code 1\n            \"\"\"\n            print(\n                'No battery found for pattern\"%s\", \\\n                available battery(ies): ' % battery_name +\n                ', '.join(self.inner.keys()),\n                file=sys.stderr\n            )\n            exit(1)\n\n        def invalid_property(\n                prop_name: str, battery_name: str, exit_code: int):\n            \"\"\"\n            Invalid property\n            :param prop_name: Name of the property\n            :param battery_name: Name of the battery\n            :param exit_code: Exit code going to be used\n            :return: Nothing, the program exits with the given status code\n            \"\"\"\n            print(\n                'Invalid property \"%s\", available properties: ' % prop_name +\n                ', '.join(self.inner[battery_name].__dict__.keys()),\n                file=sys.stderr\n            )\n            exit(exit_code)\n\n        # Parse arguments\n        args: argparse.Namespace = self.parser.parse_args(unparsed_args)\n        verb: str = str(args.verb).lower()\n        if not args.battery:\n            battery: str = 'all'\n        else:\n            battery: str = str(args.battery)\n        names: list = find_match(battery)\n\n        # Read values from the system\n        for name in names:\n            self.inner[name].read_values()\n\n        # Commands\n        if verb == 'list':\n            print(' '.join(self.inner.keys()))\n            return\n\n        if verb == 'status':\n            result: list = list()\n            for name in names:\n                result.append(self.inner[name].get_status_str())\n            if len(result) == 0:\n                invalid_battery(battery)\n            print('\\n'.join(result))\n            return\n\n        if verb.startswith('set-'):\n            if os.getuid() != 0:\n                raise NotSudo(\"Script must be run as superuser/sudo\")\n            try:\n                prop: str = verb.split('-', maxsplit=1)[1].replace('-', '_')\n            except IndexError:\n                print('Invalid command', file=sys.stderr)\n                exit(1)\n            for name in names:\n                if (prop not in EDITABLE_PROPERTIES) or\\\n                        (prop not in self.inner[name].__dict__.keys()):\n                    invalid_property(prop, name, 1)\n                value: str = ''.join(args.arguments)\n                if not value:\n                    print('Value is needed', file=sys.stderr)\n                    exit(1)\n                    return\n                self.inner[name].__dict__[prop] = int(value)\n                try:\n                    self.inner[name].set_values()\n                except ApplyValueFailedException as e:\n                    print(str(e), file=sys.stderr)\n                    exit(1)\n                print(value)\n            return\n\n        if verb.startswith('get-'):\n            try:\n                prop: str = verb.split('-', maxsplit=1)[1].replace('-', '_')\n            except IndexError:\n                print('Invalid command', file=sys.stderr)\n                exit(1)\n            result: list = list()\n            for name in names:\n                if prop not in self.inner[name].__dict__.keys():\n                    invalid_property(prop, name, 1)\n                result.append(str(self.inner[name].__dict__[prop]))\n            print(' '.join(result))\n            return\n\n        # No match found\n        print('Command \"%s\" not found' % verb, file=sys.stderr)\n        exit(1)\n"
  },
  {
    "path": "thinkpad_tools_assets/classes.py",
    "content": "import os\nimport glob\nimport sys\nimport struct\nimport subprocess\nfrom struct import pack, unpack\n\n\nclass UndervoltSystem(object):\n    def __init__(self):\n        pass\n\n    def applyUndervolt(self, mv, plane):\n        \"\"\"\n        Apply undervolt to system MSR for Intel-based systems\n        :return: int error: error code to pass\n        \"\"\"\n        error = 0\n        uv_value = format(\n            0xFFE00000 & ((round(mv*1.024) & 0xFFF) << 21), '08x').upper()\n        final_val = int((\"0x80000\" + str(plane) + \"11\" + uv_value), 16)\n        n: list = glob.glob('/dev/cpu/[0-9]*/msr')\n        for c in n:\n            f: int = os.open(c, os.O_WRONLY)\n            os.lseek(f, 0x150, os.SEEK_SET)  # MSR register 0x150\n            os.write(f, struct.pack('Q', final_val))  # Write final val\n            os.close(f)\n        if not n:\n            raise OSError(\"MSR not available. Is Secure Boot Disabled? \\\n                If not, it must be disabled for this to work.\")\n        return error\n\n    def parseReadUndervolt(self, offset):\n        plane = int(offset / (1 << 40))\n        unpack_val_unround = offset ^ (plane << 40)\n        temp = unpack_val_unround >> 21\n        unpack_val = temp if temp <= 1024 else - (2048-temp)\n        unpack_val_round = unpack_val / 1.024\n        return f\"{str(round(unpack_val_round))}\"\n\n    def readUndervolt(self, plane):\n        \"\"\"\n        Read undervolt offset on given plane\n        :return: str val: offset on plane in hex\n        \"\"\"\n        # write read to register for cpu0\n        final_val = ((1 << 63) | (plane << 40) | (1 << 36) | 0)\n        f: int = os.open('/dev/cpu/0/msr', os.O_WRONLY)\n        os.lseek(f, 0x150, os.SEEK_SET)  # MSR register 0x150\n        os.write(f, struct.pack('Q', final_val))  # Write final val\n        os.close(f)\n        # now read offset\n        f: int = os.open('/dev/cpu/0/msr', os.O_RDONLY)\n        os.lseek(f, 0x150, os.SEEK_SET)\n        offset, *_ = unpack('Q', os.read(f, 8))\n        return self.parseReadUndervolt(offset)\n"
  },
  {
    "path": "thinkpad_tools_assets/cmd.py",
    "content": "# cmd.py\n\n\"\"\"\nCommandline parser\n\"\"\"\n\n\nimport logging\nimport pathlib\nimport argparse\n\n# Setup logger\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\nBASE_DIR = pathlib.Path('/etc/thinkpad-tool/')\nDEFAULT_CONFIG_PATH = BASE_DIR / 'config.ini'\n\nUSAGE_HEAD = '''\\\nthinkpad-tools <property> <action> [<args>]\n\nSupported properties are:\n    trackpoint      Things related to TrackPoints\n    battery         Things related to batteries\n    undervolt       Things related to undervolting\n    persistence     Things related to editing persistence\n'''\n\nUSAGE_EXAMPLES = '''\\\nExamples:\n\nthinkpad-tools trackpoint status\nthinkpad-tools trackpoint set-sensitivity 20\nthinkpad-tools battery list\nthinkpad-tools battery status all\nthinkpad-tools undervolt set-core -20\nthinkpad-tools undervolt status\nthinkpad-tools persistence edit\n'''\n\n\ndef commandline_parser(unparsed_args: None or list = None):\n    \"\"\"\n    Parse the first argument and call the right handler\n    :param unparsed_args: Unparsed arguments\n    :return: Nothing\n    \"\"\"\n    parser = argparse.ArgumentParser(\n        prog='thinkpad-tools',\n        description='Tool for ThinkPads',\n        usage=USAGE_HEAD,\n        epilog=USAGE_EXAMPLES,\n        formatter_class=argparse.RawDescriptionHelpFormatter\n    )\n    parser.add_argument(\n        'property', type=str, help='Property going to take action')\n    prop = str(parser.parse_args(unparsed_args[0:1]).property).lower()\n    if prop == 'trackpoint':\n        from .trackpoint import TrackPointHandler\n        handler = TrackPointHandler()\n        handler.run(unparsed_args[1:])\n    if prop == 'battery':\n        from .battery import BatteryHandler\n        handler = BatteryHandler()\n        handler.run(unparsed_args[1:])\n    if prop == 'undervolt':\n        from .undervolt import UndervoltHandler\n        handler = UndervoltHandler()\n        handler.run(unparsed_args[1:])\n    if prop == 'persistence':\n        from .persistence import PersistenceHandler\n        handler = PersistenceHandler()\n        handler.run(unparsed_args[1:])\n"
  },
  {
    "path": "thinkpad_tools_assets/persistence.py",
    "content": "# persistence.py\n\n\"\"\"\nWrapper to edit the persistent settings\n\"\"\"\n\nimport os\nimport sys\nimport pathlib\nimport argparse\nimport configparser\nimport thinkpad_tools_assets.classes\nfrom thinkpad_tools_assets.cmd import commandline_parser\nfrom thinkpad_tools_assets.utils import NotSudo\n\ntry:\n    if os.getuid() != 0:\n        raise NotSudo(\"Script must be run as superuser/sudo\")\nexcept NotSudo:\n    print(\"ERROR: This script must be run as superuser/sudo\")\n    sys.exit(1)\n\nUSAGE_HEAD: str = '''\\\nthinkpad-tools persistence <verb>\n\nSupported verbs are:\n    edit    Edit the persistent settings\n    enable  Enable persistent settings\n    disable Disable persistent settings\n    apply   Apply the persistent settings\n'''\n\nUSAGE_EXAMPLES: str = '''\\\nExamples:\n\nthinkpad-tools persistence edit\nthinkpad-tools persistence disable\nthinkpad-tools persistence enable\nthinkpad-tools persistence apply\n'''\n\n\nclass PersistenceHandler(object):\n    \"\"\"\n    Handler for Undervolt related commands\n    \"\"\"\n    def __init__(self):\n        self.parser: argparse.ArgumentParser = argparse.ArgumentParser(\n            prog='thinkpad-tools persistence',\n            description='Edit persistence settings',\n            usage=USAGE_HEAD,\n            epilog=USAGE_EXAMPLES,\n            formatter_class=argparse.RawDescriptionHelpFormatter\n        )\n        self.parser.add_argument('verb', type=str, help='The action going to \\\n            take')\n\n    def run(self, unparsed_args: list):\n        \"\"\"\n        Parse and execute the command\n        :param unparsed_args: Unparsed arguments for this property\n        :return: Nothing\n        \"\"\"\n        def invalid_property(prop_name: str, exit_code: int):\n            \"\"\"\n            Print error message and exit with exit code 1\n            :param prop_name: Name of the property\n            :param exit_code: Exit code\n            :return: Nothing, the problem exits with the given exit code\n            \"\"\"\n            print(\n                'Invalid command \"%s\", available properties: ' % prop_name +\n                ', '.join(self.inner.__dict__.keys()),\n                file=sys.stderr\n            )\n            exit(exit_code)\n\n        # Parse arguments\n        args: argparse.Namespace = self.parser.parse_args(unparsed_args)\n        verb: str = str(args.verb).lower()\n\n        # Commands\n        if verb == 'edit':\n            try:\n                editor: str = os.environ['EDITOR']\n            except KeyError:\n                editor: str = \"/usr/bin/nano\"\n            os.system('sudo {editor} /etc/thinkpad-tools.ini'\n                      .format(editor=editor))\n            return\n        if verb == \"enable\":\n            os.system('systemctl daemon-reload')\n            os.system('systemctl enable thinkpad-tools.service')\n            print(\"\"\"To set persistent settings, please edit the file\n                     '/etc/thinkpad-tools.ini'\"\"\")\n            print(\"Persistence enabled\")\n            return\n        if verb == \"disable\":\n            os.system('systemctl daemon-reload')\n            os.system('systemctl disable thinkpad-tools.service')\n            print(\"Persistence disabled\")\n            return\n        if verb == \"apply\":\n            config: configparser.ConfigParser = configparser.ConfigParser()\n            config.read('/etc/thinkpad-tools.ini')\n            for section in config.sections():\n                for (command, val) in config.items(section):\n                    commandline_parser([section, \"set-\"+command, val])\n            return\n\n        # No match found\n        print('Command \"%s\" not found' % verb, file=sys.stderr)\n        exit(1)\n"
  },
  {
    "path": "thinkpad_tools_assets/thinkpad-tools.ini",
    "content": "# [trackpoint]\n# speed = 255\n# sensitivity = 255\n# [undervolt]\n# [battery]"
  },
  {
    "path": "thinkpad_tools_assets/thinkpad-tools.service",
    "content": "[Unit]\nDescription=Thinkpad Tools Persistence Service\nAfter=multi-user.target\n\n[Service]\nType=oneshot\nUser=root\nGroup=root\nExecStart=thinkpad-tools persistence apply\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "thinkpad_tools_assets/trackpoint.py",
    "content": "# trackpoint.py\n\n\"\"\"\nTrackpoint related stuff\n\"\"\"\n\nfrom thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo\nimport os\nimport sys\nimport pathlib\nimport argparse\n\ntry:\n    if os.getuid() != 0:\n        raise NotSudo(\"Script must be run as superuser/sudo\")\nexcept NotSudo:\n    print(\"ERROR: This script must be run as superuser/sudo\")\n    sys.exit(1)\n\nif os.path.exists(\"/sys/devices/rmi4-00/rmi4-00.fn03/serio2\"):\n    BASE_PATH = pathlib.PurePath('/sys/devices/rmi4-00/rmi4-00.fn03/serio2')\nelif os.path.exists(\"/sys/devices/rmi4-00/rmi4-00.fn03/serio3\"):\n    BASE_PATH = pathlib.PurePath('/sys/devices/rmi4-00/rmi4-00.fn03/serio3')\nelse:\n    BASE_PATH = pathlib.PurePath('/sys/devices/platform/i8042/serio1/serio2')\n\nSTATUS_TEXT = '''\\\nCurrent status:\n  Sensitivity:             {sensitivity}\n  Speed:                   {speed}\\\n'''\n\nUSAGE_HEAD: str = '''\\\nthinkpad-tools trackpoint <verb> [argument]\n\nSupported verbs are:\n    status              Print all properties\n    set-<property>      Set value\n    get-<property>      Get property\n    disable             Disable trackpoint\nAvailable properties: sensitivity, speed\n'''\n\nUSAGE_EXAMPLES: str = '''\\\nExamples:\n\nthinkpad-tools trackpoint status\nthinkpad-tools trackpoint set-sensitivity 20\nthinkpad-tools trackpoint get-speed\nthinkpad-tools trackpoint disable\n'''\n\n\nclass TrackPoint(object):\n    \"\"\"\n    Class to handle requests related to TrackPoints\n    \"\"\"\n\n    def __init__(\n            self,\n            sensitivity: int or None = None,\n            speed: int or None = None\n    ):\n        self.sensitivity = sensitivity\n        self.speed = speed\n\n    def read_values(self):\n        \"\"\"\n        Read values from the system\n        :return: Nothing\n        \"\"\"\n        for prop in self.__dict__.keys():\n            file_path: str = str(BASE_PATH / prop)\n            if os.path.isfile(file_path):\n                with open(file_path) as file:\n                    self.__dict__[prop] = file.readline()\n            else:\n                self.__dict__[prop] = None\n\n    def set_values(self):\n        \"\"\"\n        Set values to the system\n        :return: Nothing\n        \"\"\"\n        success: bool = True\n        failures: list = list()\n        for prop in self.__dict__.keys():\n            file_path: str = str(BASE_PATH / prop)\n            if os.path.isfile(file_path):\n                try:\n                    with open(file_path, 'w') as file:\n                        file.write(self.__dict__[prop])\n                except Exception as e:\n                    success = False\n                    failures.append(str(e))\n        if not success:\n            raise ApplyValueFailedException(', '.join(failures))\n\n    def disableTrackpoint(self):\n        \"\"\"\n        Disable the trackpoint\n        :return: Nothing\n        \"\"\"\n        success: bool = True\n        failures: list = list()\n        for prop in self.__dict__.keys():\n            file_path: str = str(BASE_PATH / prop)\n            if os.path.isfile(file_path):\n                try:\n                    with open(file_path, 'w') as file:\n                        file.write('0')\n                except Exception as e:\n                    success = False\n                    failures.append(str(e))\n        if not success:\n            raise ApplyValueFailedException(', '.join(failures))\n\n    def get_status_str(self) -> str:\n        \"\"\"\n        Return status string\n        :return: str: status string\n        \"\"\"\n        return STATUS_TEXT.format(\n            sensitivity=self.sensitivity or 'Unknown',\n            speed=self.speed or 'Unknown'\n        )\n\n\nclass TrackPointHandler(object):\n    \"\"\"\n    Handler for TrackPoint related commands\n    \"\"\"\n\n    def __init__(self):\n        self.parser: argparse.ArgumentParser = argparse.ArgumentParser(\n            prog='thinkpad-tools trackpoint',\n            description='TrackPoint related commands',\n            usage=USAGE_HEAD,\n            epilog=USAGE_EXAMPLES,\n            formatter_class=argparse.RawDescriptionHelpFormatter\n        )\n        self.parser.add_argument(\n            'verb', type=str, help='The action going to take')\n        self.parser.add_argument(\n            'arguments', nargs='*', help='Arguments of the action')\n        self.inner: TrackPoint = TrackPoint()\n\n    def run(self, unparsed_args: list):\n        \"\"\"\n        Parse and execute the command\n        :param unparsed_args: Unparsed arguments for this property\n        :return: Nothing\n        \"\"\"\n        def invalid_property(prop_name: str, exit_code: int):\n            \"\"\"\n            Print error message and exit with exit code 1\n            :param prop_name: Name of the property\n            :param exit_code: Exit code\n            :return: Nothing, the problem exits with the given exit code\n            \"\"\"\n            print(\n                'Invalid command \"%s\", available properties: ' % prop_name +\n                ', '.join(self.inner.__dict__.keys()),\n                file=sys.stderr\n            )\n            exit(exit_code)\n\n        # Parse arguments\n        args: argparse.Namespace = self.parser.parse_args(unparsed_args)\n        verb: str = str(args.verb).lower()\n\n        # Read values from the system\n        self.inner.read_values()\n\n        # Commands\n        if verb == 'status':\n            print(self.inner.get_status_str())\n            return\n\n        if verb.startswith('set-'):\n            try:\n                prop: str = verb.split('-', maxsplit=1)[1]\n            except IndexError:\n                invalid_property(verb, 1)\n                return\n            if prop not in self.inner.__dict__.keys():\n                invalid_property(prop, 1)\n            self.inner.__dict__[prop] = str(''.join(args.arguments))\n            self.inner.set_values()\n            print(self.inner.get_status_str())\n            return\n\n        if verb.startswith('get-'):\n            try:\n                prop: str = verb.split('-', maxsplit=1)[1]\n            except IndexError:\n                invalid_property(verb, 1)\n                return\n            if not hasattr(self.inner, prop):\n                invalid_property(prop, 1)\n            if not self.inner.__dict__[prop]:\n                print('Unable to read %s' % prop)\n                exit(1)\n            print(self.inner.__dict__[prop])\n            return\n        if verb == 'disable':\n            self.inner.disableTrackpoint()\n            print(self.inner.get_status_str())\n            return\n        # No match found\n        print('Command \"%s\" not found' % verb, file=sys.stderr)\n        exit(1)\n"
  },
  {
    "path": "thinkpad_tools_assets/undervolt.py",
    "content": "# undervolt.py\n\n\"\"\"\nUndervolt related stuff\n\"\"\"\n\nimport os\nimport sys\nimport pathlib\nimport argparse\nimport thinkpad_tools_assets.classes\nfrom thinkpad_tools_assets.utils import ApplyValueFailedException, NotSudo\n\n\ntry:\n    if os.getuid() != 0:\n        raise NotSudo(\"Script must be run as superuser/sudo\")\nexcept NotSudo:\n    print(\"ERROR: This script must be run as superuser/sudo\")\n    sys.exit(1)\n\n# PLANE KEY:\n# Plane 0: Core\n# Plane 1: GPU\n# Plane 2: Cache\n# Plane 3: Uncore\n# Plane 4: Analogio\n\nSTATUS_TEXT = '''\\\nCurrent status:\n  Core:                    {core}\\n\n  GPU:                     {gpu}\\n\n  Cache:                   {cache}\\n\n  Uncore:                  {uncore}\\n\n  Analogio:                {analogio}\\n\n'''\nUSAGE_HEAD: str = '''\\\nthinkpad-tools undervolt <verb> [argument]\n\nSupported verbs are:\n    status          Print all properties\n    set-<property>  Set value\n    get-<property>  Get property\nAvailable properties: core, gpu, cache, uncore, analogio\n'''\n\nUSAGE_EXAMPLES: str = '''\\\nExamples:\n\nthinkpad-tools trackpoint status\nthinkpad-tools trackpoint set-core -20\nthinkpad-tools trackpoint get-gpu\n'''\n\n\nclass Undervolt(object):\n    \"\"\"\n    Class to handle requests related to Undervolting\n    \"\"\"\n\n    def __init__(\n            self,\n            core: float or None = None,\n            gpu: float or None = None,\n            cache: float or None = None,\n            uncore: float or None = None,\n            analogio: float or None = None,\n    ):\n        # self.__register: str = \"0x150\"\n        # self.__undervolt_value: str = \"0x80000\"\n        self.core = core\n        self.gpu = gpu\n        self.cache = cache\n        self.uncore = uncore\n        self.analogio = analogio\n\n    def read_values(self):\n        \"\"\"\n        Read values from the system\n        :return: Nothing\n        \"\"\"\n        success = True\n        failures: list = list()\n        system = thinkpad_tools_assets.classes.UndervoltSystem()\n        for prop in self.__dict__.keys():\n            plane_hashmap = {\"core\": 0, \"gpu\": 1, \"cache\": 2, \"uncore\": 3, \"analogio\": 4}\n            h: str = ''\n            try:\n                plane = plane_hashmap[prop]\n                h = system.readUndervolt(plane)\n            except Exception as e:\n                success = False\n                failures.append(str(e))\n            self.__dict__[prop] = h\n        if not success:\n            raise ApplyValueFailedException(', '.join(failures))\n\n    def set_values(self):\n        \"\"\"\n        Set values to the system MSR using undervolt function\n        :return: Nothing\n        \"\"\"\n        system = thinkpad_tools_assets.classes.UndervoltSystem()\n        success: bool = True\n        failures: list = list()\n        plane_hashmap = {\"core\": 0, \"gpu\": 1, \"cache\": 2, \"uncore\": 3, \"analogio\": 4}\n        for prop in self.__dict__.keys():\n            if self.__dict__[prop] is None:\n                continue\n            try:\n                plane: int = plane_hashmap[prop]\n                system.applyUndervolt(int(self.__dict__[prop]), plane)\n            except Exception as e:\n                success = False\n                failures.append(str(e))\n        if not success:\n            raise ApplyValueFailedException(', '.join(failures))\n\n    def get_status_str(self) -> str:\n        \"\"\"\n        Return status string\n        :return: str: status string\n        \"\"\"\n        return STATUS_TEXT.format(\n            core=self.core,\n            gpu=self.gpu,\n            cache=self.cache,\n            uncore=self.uncore,\n            analogio=self.analogio\n        )\n\n\nclass UndervoltHandler(object):\n    \"\"\"\n    Handler for Undervolt related commands\n    \"\"\"\n    def __init__(self):\n        self.parser: argparse.ArgumentParser = argparse.ArgumentParser(\n            prog='thinkpad-tools undervolt',\n            description='Undervolt related commands',\n            usage=USAGE_HEAD,\n            epilog=USAGE_EXAMPLES,\n            formatter_class=argparse.RawDescriptionHelpFormatter\n        )\n        self.parser.add_argument('verb', type=str, help='The action going to \\\n            take')\n        self.parser.add_argument(\n            'arguments', nargs='*', help='Arguments of the action')\n        self.inner: Undervolt = Undervolt()\n\n    def run(self, unparsed_args: list):\n        \"\"\"\n        Parse and execute the command\n        :param unparsed_args: Unparsed arguments for this property\n        :return: Nothing\n        \"\"\"\n        def invalid_property(prop_name: str, exit_code: int):\n            \"\"\"\n            Print error message and exit with exit code 1\n            :param prop_name: Name of the property\n            :param exit_code: Exit code\n            :return: Nothing, the problem exits with the given exit code\n            \"\"\"\n            print(\n                'Invalid command \"%s\", available properties: ' % prop_name +\n                ', '.join(self.inner.__dict__.keys()),\n                file=sys.stderr\n            )\n            exit(exit_code)\n\n        # Parse arguments\n        args: argparse.Namespace = self.parser.parse_args(unparsed_args)\n        verb: str = str(args.verb).lower()\n\n        # Read values from the system\n        self.inner.read_values()\n\n        # Commands\n        if verb == 'status':\n            print(self.inner.get_status_str())\n            return\n\n        if verb.startswith('set-'):\n            try:\n                prop: str = verb.split('-', maxsplit=1)[1]\n            except IndexError:\n                invalid_property(verb, 1)\n                return\n            if prop not in self.inner.__dict__.keys():\n                invalid_property(prop, 1)\n            self.inner.__dict__[prop] = str(''.join(args.arguments))\n            self.inner.set_values()\n            print(self.inner.get_status_str())\n            return\n\n        if verb.startswith('get-'):\n            try:\n                prop: str = verb.split('-', maxsplit=1)[1]\n            except IndexError:\n                invalid_property(verb, 1)\n            if not hasattr(self.inner, prop):\n                invalid_property(prop, 1)\n            if not self.inner.__dict__[prop]:\n                print('Unable to read %s' % prop)\n                exit(1)\n            print(self.inner.__dict__[prop])\n            return\n\n        # No match found\n        print('Command \"%s\" not found' % verb, file=sys.stderr)\n        exit(1)\n"
  },
  {
    "path": "thinkpad_tools_assets/utils.py",
    "content": "# thinkpad_tools_assets.utils.py\n\n\nclass ApplyValueFailedException(Exception):\n    \"\"\"\n    Exception raised when failed to apply settings\n    \"\"\"\n    pass\n\n\nclass NotSudo(Exception):\n    pass\n"
  },
  {
    "path": "upload-pypi.sh",
    "content": "#!/bin/bash\nsudo rm -rf dist && sudo python3 setup.py sdist && twine upload dist/*"
  }
]