├── .cursor └── rules │ └── riper5.mdc ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .gitmodules ├── .shellcheckrc ├── .vscode └── settings.json ├── LICENSE ├── Makefile ├── README.md ├── README_en.md ├── assets ├── 20230107182755_1.jpg ├── 20230107182852_1.jpg ├── 20230108234559_1.jpg ├── 20230110153822_1.jpg ├── 20230111054307_1.jpg └── 20230319145542_1.jpg ├── backend └── sh_tools.sh ├── bin └── ryzenadj ├── decky.pyi ├── install.sh ├── main.py ├── package.json ├── plugin.json ├── pnpm-lock.yaml ├── py_modules ├── conf_manager.py ├── config.py ├── cpu.py ├── devices │ ├── README.md │ ├── __init__.py │ ├── asus │ │ └── asus_device.py │ ├── ayaneo │ │ ├── ayaneo_air.py │ │ ├── ayaneo_air_1s.py │ │ ├── ayaneo_air_plus.py │ │ ├── ayaneo_air_plus_intel.py │ │ ├── ayaneo_air_plus_mendocino.py │ │ ├── ayaneo_device.py │ │ ├── ayaneo_device_ii.py │ │ ├── ayaneo_flip_ds.py │ │ ├── ayaneo_flip_kb.py │ │ └── ayaneo_kun.py │ ├── firmware_attribute_device.py │ ├── idevice.py │ ├── lenovo │ │ └── lenovo_device.py │ ├── msi │ │ ├── msi_claw8.py │ │ ├── msi_claw_a1m.py │ │ └── msi_device.py │ └── power_device.py ├── ec.py ├── fan.py ├── fan_config │ ├── ec │ │ ├── aokzoe_a.yml │ │ ├── ayaneo_2.yml │ │ ├── ayaneo_air.yml │ │ ├── ayaneo_airplus.yml │ │ ├── gpd_win4.yml │ │ ├── gpd_win4_ver1.yml │ │ ├── gpd_winmax2.yml │ │ ├── gpd_winmini.yml │ │ ├── onexplayer2.yml │ │ └── onexplayer_mini.yml │ ├── hwmon │ │ ├── asus.yml │ │ ├── asus_orig.yml │ │ ├── aynec.yml │ │ ├── gpdfan.yml │ │ ├── msi_ec.yml │ │ ├── msi_wmi.yml │ │ ├── oxp_ec.yml │ │ ├── oxpec.yml │ │ └── steamdeck.yml │ └── schema │ │ ├── ec.json │ │ └── hwmon.json ├── fuse_manager.py ├── gpu.py ├── inotify.py ├── logging_handler.py ├── pfuse │ ├── README.md │ ├── __init__.py │ ├── driver.py │ └── utils.py ├── portio.so ├── power_manager.py ├── sysInfo.py ├── tt.sh ├── update.py └── utils │ ├── __init__.py │ ├── battery.py │ ├── gpu_fix.py │ └── tdp.py ├── rollup.config.js ├── src ├── components │ ├── SlowSliderField.tsx │ ├── actionButtonItem.tsx │ ├── cpu.tsx │ ├── customTDP.tsx │ ├── fan.tsx │ ├── fanCanvas.tsx │ ├── gpu.tsx │ ├── index.ts │ ├── more.tsx │ ├── power.tsx │ └── settings.tsx ├── i18n │ ├── bulgarian.json │ ├── english.json │ ├── french.json │ ├── german.json │ ├── index.ts │ ├── italian.json │ ├── japanese.json │ ├── koreana.json │ ├── localization.ts │ ├── localizeMap.ts │ ├── schinese.json │ ├── tchinese.json │ └── thai.json ├── index.tsx ├── tab │ ├── index.ts │ ├── tabCpu.tsx │ ├── tabFans.tsx │ ├── tabGpu.tsx │ ├── tabMore.tsx │ └── tabPower.tsx ├── types │ └── global.d.ts └── util │ ├── backend.ts │ ├── enum.ts │ ├── index.ts │ ├── logger.ts │ ├── patch.ts │ ├── pluginMain.ts │ ├── position.ts │ ├── settings.ts │ ├── steamClient.ts │ ├── steamUtils.tsx │ ├── timeout.ts │ └── version.ts └── tsconfig.json /.cursor/rules/riper5.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # RIPER-5 模式:严格操作协议 7 | 8 | ## 背景介绍 9 | 10 | 你是Claude 3.7,你被集成到Cursor IDE中,这是一个基于AI的VS Code分支。由于你的高级能力,你往往过于热情,经常在没有明确请求的情况下实施更改,通过假设你比我更了解而破坏现有逻辑。这会导致代码的不可接受的灾难。在处理我的代码库时——无论是网络应用、数据管道、嵌入式系统还是任何其他软件项目——你未经授权的修改可能会引入细微的错误并破坏关键功能。为防止这种情况,你必须遵循这个严格协议: 11 | 12 | ## 元指令:模式声明要求 13 | 14 | 你必须在每个回复的开头用括号声明你的当前模式。没有例外。格式:[MODE: MODE_NAME] 未能声明你的模式是对协议的严重违反。并且不能自主转换模式! 15 | 16 | ## RIPER-5 模式 17 | 18 | ### 模式 1:研究 19 | [MODE: RESEARCH] 20 | 21 | - **目的**:仅收集信息 22 | - **允许**:阅读文件,提出澄清问题,理解代码结构 23 | - **禁止**:建议,实施,计划,或任何行动暗示 24 | - **要求**:你只能寻求理解现有内容,而不是可能的内容 25 | - **持续时间**:直到我明确指示转到下一个模式 26 | - **输出格式**:以[MODE: RESEARCH]开始,然后只有观察和问题 27 | 28 | ### 模式 2:创新 29 | [MODE: INNOVATE] 30 | 31 | - **目的**:头脑风暴潜在方法 32 | - **允许**:讨论想法,优势/劣势,寻求反馈 33 | - **禁止**:具体计划,实施细节,或任何代码编写 34 | - **要求**:所有想法必须作为可能性呈现,而非决定 35 | - **持续时间**:直到我明确指示转到下一个模式 36 | - **输出格式**:以[MODE: INNOVATE]开始,然后只有可能性和考虑因素 37 | 38 | ### 模式 3:计划 39 | [MODE: PLAN] 40 | 41 | - **目的**:创建详尽的技术规范 42 | - **允许**:详细计划,包含确切的文件路径,函数名和更改 43 | - **禁止**:任何实施或代码编写,即使是"示例代码" 44 | - **要求**:计划必须足够全面,使实施过程中不需要创造性决策 45 | - **强制最终步骤**:将整个计划转换为编号的、顺序的检查清单,每个原子操作作为单独的项目 46 | - **检查清单格式**: 47 | 实施检查清单: 48 | 1. [具体操作1] 49 | 2. [具体操作2] 50 | ... 51 | n. [最终操作] 52 | - **持续时间**:直到我明确批准计划并指示转到下一个模式 53 | - **输出格式**:以[MODE: PLAN]开始,然后只有规范和实施细节 54 | 55 | ### 模式 4:执行 56 | [MODE: EXECUTE] 57 | 58 | - **目的**:精确实施模式3中计划的内容 59 | - **允许**:只实施批准计划中明确详述的内容 60 | - **禁止**:任何偏离、改进或不在计划中的创新补充 61 | - **进入要求**:只有在我明确命令"ENTER EXECUTE MODE"后才能进入 62 | - **偏差处理**:如果发现任何需要偏离的问题,立即返回PLAN模式 63 | - **输出格式**:以[MODE: EXECUTE]开始,然后只有符合计划的实施 64 | 65 | ### 模式 5:审查 66 | [MODE: REVIEW] 67 | 68 | - **目的**:无情地验证实施是否符合计划 69 | - **允许**:对计划和实施进行逐行比较 70 | - **要求**:明确标记任何偏差,无论多么微小 71 | - **偏差格式**:":warning: 检测到偏差:[确切偏差描述]" 72 | - **报告**:必须报告实施是否与计划完全相同 73 | - **结论格式**:":white_check_mark: 实施完全符合计划"或":cross_mark: 实施偏离计划" 74 | - **输出格式**:以[MODE: REVIEW]开始,然后是系统比较和明确判断 75 | 76 | ## 关键协议指南 77 | 78 | 1. 未经我明确许可,你不能在模式之间转换 79 | 2. 你必须在每次回复的开头声明你的当前模式 80 | 3. 在EXECUTE模式下,你必须100%忠实地遵循计划 81 | 4. 在REVIEW模式下,你必须标记即使是最小的偏差 82 | 5. 你无权在声明的模式之外做出独立决定 83 | 6. 未能遵循此协议将对我的代码库造成灾难性后果 84 | 7. 始终使用中文回答 85 | 8. git 提交信息使用英文,优先使用 xxx: xxxxxxxxxx 这样的形式 86 | 9. 代码注释使用英文 87 | 88 | ## 模式转换信号 89 | 90 | 必须注意!!只有当我明确发出以下信号时才转换模式: 91 | 92 | "进入研究模式" 93 | "进入创新模式" 94 | "进入计划模式" 95 | "进入执行模式" 96 | "进入审查模式" 97 | 98 | 没有这些确切信号,保持在当前模式。 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: 10 | - "main" 11 | tags: 12 | - "v*.*.*" 13 | 14 | jobs: 15 | build_plugin: 16 | runs-on: ubuntu-latest 17 | container: 18 | image: archlinux:latest 19 | steps: 20 | - name: set git global safe directory 21 | run: | 22 | pacman -Syu git npm tree zip unzip --noconfirm 23 | git config --global --add safe.directory $(realpath .) 24 | 25 | - uses: actions/checkout@v4 26 | 27 | - name: update submodules 28 | run: git submodule update --init --recursive 29 | 30 | - name: build RyzenAdj 31 | run: | 32 | pacman -S base-devel pciutils cmake --noconfirm --needed --overwrite='*' 33 | cd submodule/RyzenAdj 34 | mkdir build && cd build 35 | cmake -DCMAKE_BUILD_TYPE=Release .. 36 | make 37 | cp -f ryzenadj ../../../bin 38 | 39 | # - name: build python-fuse 40 | # run: | 41 | # PWD=$(pwd) 42 | # echo "PWD: $PWD" 43 | # mkdir -p py_modules/site-packages 44 | # pacman -S python-fuse python-setuptools python-wheel python-installer rsync --noconfirm --needed --overwrite='*' 45 | # cd ${PWD}/submodule/python-fuse && \ 46 | # PYTHONPATH=${PWD}/py_modules/site-packages \ 47 | # python3 setup.py install --prefix=${PWD} --install-lib=install 48 | # cd - 49 | # rsync -av --progress --exclude=*.pyc --exclude=__pycache__ ./submodule/python-fuse/install/fuse*/fuse* ./py_modules/site-packages/ 50 | 51 | - name: change log level 52 | run: | 53 | sed -i 's/logging.DEBUG/logging.INFO/' py_modules/config.py 54 | 55 | - name: build plugin 56 | run: | 57 | npm i -g pnpm 58 | pnpm install --no-frozen-lockfile 59 | pnpm update 60 | pnpm run build 61 | tar -czvf PowerControl.tar.gz --transform 's,^,PowerControl/,' dist backend py_modules bin *.py *.json *.md *.js LICENSE 62 | # zip 63 | mkdir -p PowerControl 64 | cp -r dist backend py_modules bin *.py *.json *.md *.js LICENSE PowerControl 65 | zip -r PowerControl.zip PowerControl 66 | rm -rf PowerControl 67 | 68 | - name: show files 69 | run: | 70 | tar -tzvf PowerControl.tar.gz 71 | unzip -l PowerControl.zip 72 | - name: Publish Artifacts 73 | uses: actions/upload-artifact@v4 74 | with: 75 | name: PowerControl 76 | path: | 77 | PowerControl.tar.gz 78 | PowerControl.zip 79 | 80 | publish: 81 | if: startsWith(github.ref, 'refs/tags/v') 82 | runs-on: ubuntu-latest 83 | needs: build_plugin 84 | steps: 85 | - run: mkdir /tmp/artifacts 86 | 87 | - name: download artifact 88 | uses: actions/download-artifact@v4 89 | with: 90 | path: /tmp/artifacts 91 | 92 | - run: ls -R /tmp/artifacts 93 | 94 | - name: publish to github release 95 | uses: softprops/action-gh-release@v2 96 | with: 97 | files: | 98 | /tmp/artifacts/PowerControl/PowerControl.tar.gz 99 | /tmp/artifacts/PowerControl/PowerControl.zip 100 | tag_name: ${{ github.ref_name }} 101 | draft: false 102 | generate_release_notes: true 103 | prerelease: contains(github.ref, 'pre') 104 | env: 105 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | # Coverage reports 17 | coverage 18 | 19 | # API keys and secrets 20 | .env 21 | 22 | # Dependency directory 23 | node_modules 24 | bower_components 25 | # py_modules 26 | py_modules/site-packages 27 | 28 | # Editors 29 | .idea 30 | *.iml 31 | 32 | # OS metadata 33 | .DS_Store 34 | Thumbs.db 35 | 36 | # Ignore built ts files 37 | dist/ 38 | 39 | __pycache__/ 40 | 41 | /.yalc 42 | yalc.lock 43 | 44 | # .vscode/settings.json 45 | 46 | # Ignore output folder 47 | 48 | backend/out 49 | backend/*.sh 50 | # Makefile 51 | 52 | .history -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "submodule/RyzenAdj"] 2 | path = submodule/RyzenAdj 3 | url = https://github.com/FlyGoat/RyzenAdj.git 4 | [submodule "submodule/python-fuse"] 5 | path = submodule/python-fuse 6 | url = https://github.com/libfuse/python-fuse.git 7 | -------------------------------------------------------------------------------- /.shellcheckrc: -------------------------------------------------------------------------------- 1 | disable=SC1090,SC1091,SC2086,SC2034,SC2148 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.extraPaths": [ 3 | "../decky-loader/backend/decky_loader/plugin/imports", 4 | "../decky-loader/backend/decky_loader", 5 | "./py_modules", 6 | "./backend" 7 | ], 8 | "yaml.schemas": { 9 | "./py_modules/fan_config/schema/ec.json": ["py_modules/fan_config/ec/*.yml"], 10 | "./py_modules/fan_config/schema/hwmon.json": ["py_modules/fan_config/hwmon/*.yml"], 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Gawah 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ifneq (,$(wildcard ./.env)) 2 | include .env 3 | export 4 | endif 5 | 6 | SHELL=bash 7 | 8 | help: ## Display list of tasks with descriptions 9 | @echo "+ $@" 10 | @fgrep -h ": ## " $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed 's/-default//' | awk 'BEGIN {FS = ": ## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 11 | 12 | vendor: ## Install project dependencies 13 | @echo "+ $@" 14 | @pnpm i 15 | 16 | env: ## Create default .env file 17 | @echo "+ $@" 18 | @echo -e '# Makefile tools\nDECK_USER=deck\nDECK_HOST=\nDECK_PORT=22\nDECK_HOME=/home/deck\nDECK_KEY=~/.ssh/id_rsa' >> .env 19 | @echo -n "PLUGIN_FOLDER=" >> .env 20 | @jq -r .name package.json >> .env 21 | 22 | init: ## Initialize project 23 | @$(MAKE) env 24 | @$(MAKE) vendor 25 | @echo -e "\n\033[1;36m Almost ready! Just a few things left to do:\033[0m\n" 26 | @echo -e "1. Open .env file and make sure every DECK_* variable matches your steamdeck's ip/host, user, etc" 27 | @echo -e "2. Run \`\033[0;36mmake copy-ssh-key\033[0m\` to copy your public ssh key to steamdeck" 28 | @echo -e "3. Build your code with \`\033[0;36mmake build\033[0m\` or \`\033[0;36mmake docker-build\033[0m\` to build inside a docker container" 29 | @echo -e "4. Deploy your plugin code to steamdeck with \`\033[0;36mmake deploy\033[0m\`" 30 | 31 | update-frontend-lib: ## Update decky-frontend-lib 32 | @echo "+ $@" 33 | @pnpm update decky-frontend-lib --latest 34 | 35 | update-ui: ## Update @decky/api and @decky/ui 36 | @echo "+ $@" 37 | @pnpm update @decky/api @decky/ui react-icons --latest 38 | 39 | 40 | update-decky-ui: ## Update @decky/ui @decky/api 41 | @echo "+ $@" 42 | @pnpm update @decky/ui --latest 43 | @pnpm update @decky/api --latest 44 | 45 | build-front: ## Build frontend 46 | @echo "+ $@" 47 | @pnpm run build 48 | 49 | build-back: ## Build backend 50 | @echo "+ $@" 51 | @make -C ./backend 52 | 53 | build: ## Build everything 54 | @$(MAKE) build-front build-back 55 | 56 | copy-ssh-key: ## Copy public ssh key to steamdeck 57 | @echo "+ $@" 58 | @ssh-copy-id -i $(DECK_KEY) $(DECK_USER)@$(DECK_HOST) 59 | 60 | deploy-steamdeck: ## Deploy plugin build to steamdeck 61 | @echo "+ $@" 62 | @ssh $(DECK_USER)@$(DECK_HOST) -p $(DECK_PORT) -i $(DECK_KEY) \ 63 | 'chmod -v 755 $(DECK_HOME)/homebrew/plugins/ && mkdir -p $(DECK_HOME)/homebrew/plugins/$(PLUGIN_FOLDER)' 64 | @rsync -azp --delete --progress -e "ssh -p $(DECK_PORT) -i $(DECK_KEY)" \ 65 | --chmod=Du=rwx,Dg=rx,Do=rx,Fu=rwx,Fg=rx,Fo=rx \ 66 | --exclude='.git/' \ 67 | --exclude='*.pyc' \ 68 | --exclude='.github/' \ 69 | --exclude='.vscode/' \ 70 | --exclude='node_modules/' \ 71 | --exclude='.pnpm-store/' \ 72 | --exclude='src/' \ 73 | --exclude='*.log' \ 74 | --exclude='.gitignore' . \ 75 | --exclude='.idea' . \ 76 | --exclude='.env' . \ 77 | --exclude='Makefile' . \ 78 | --exclude='submodule' . \ 79 | --exclude='__pycache__' . \ 80 | ./ $(DECK_USER)@$(DECK_HOST):$(DECK_HOME)/homebrew/plugins/$(PLUGIN_FOLDER)/ 81 | @ssh $(DECK_USER)@$(DECK_HOST) -p $(DECK_PORT) -i $(DECK_KEY) \ 82 | 'chmod -v 755 $(DECK_HOME)/homebrew/plugins/' 83 | 84 | local-fuse: ## Copy fuse module to local site-packages 85 | @echo "+ $@" 86 | @mkdir -p ./py_modules/site-packages 87 | @rm -rf ./py_modules/site-packages/fuse* ./py_modules/site-packages/fuseparts 88 | @rm -rf ./submodule/python-fuse/build 89 | @cd ./submodule/python-fuse && \ 90 | PYTHONPATH=$(PWD)/py_modules/site-packages \ 91 | python3 setup.py install --prefix=dist/install --install-lib=dist/install && \ 92 | rsync -a --exclude=*.pyc --exclude=__pycache__ ./dist/install/fuse*/fuse* $(PWD)/py_modules/site-packages/ 93 | @rm -rf $(PWD)/submodule/python-fuse/build 94 | 95 | 96 | restart-decky: ## Restart Decky on remote steamdeck 97 | @echo "+ $@" 98 | @ssh -t $(DECK_USER)@$(DECK_HOST) -p $(DECK_PORT) -i $(DECK_KEY) \ 99 | 'sudo systemctl restart plugin_loader.service' 100 | @echo -e '\033[0;32m+ all is good, restarting Decky...\033[0m' 101 | 102 | deploy: ## Deploy code to steamdeck and restart Decky 103 | @$(MAKE) deploy-steamdeck 104 | @$(MAKE) restart-decky 105 | 106 | deploy-only: ## Deploy code to steamdeck 107 | @$(MAKE) deploy-steamdeck 108 | @$(MAKE) set-loglevel 109 | 110 | deploy-only-debug: ## Deploy code to steamdeck 111 | @$(MAKE) deploy-steamdeck 112 | 113 | deploy-release: ## Deploy release to steamdeck and restart Decky 114 | @$(MAKE) deploy-steamdeck 115 | @$(MAKE) set-loglevel 116 | @$(MAKE) restart-decky 117 | 118 | set-loglevel: ## Set log level to info 119 | @echo "+ $@" 120 | @ssh -t $(DECK_USER)@$(DECK_HOST) -p $(DECK_PORT) -i $(DECK_KEY) \ 121 | 'chmod -v 755 $(DECK_HOME)/homebrew/plugins/$(PLUGIN_FOLDER)/py_modules' 122 | @ssh -t $(DECK_USER)@$(DECK_HOST) -p $(DECK_PORT) -i $(DECK_KEY) \ 123 | "sed -i 's/logging.DEBUG/logging.INFO/' $(DECK_HOME)/homebrew/plugins/$(PLUGIN_FOLDER)/py_modules/config.py" 124 | 125 | it: ## Build all code, deploy it to steamdeck, restart Decky 126 | @$(MAKE) build deploy 127 | 128 | cleanup: ## Delete all generated files and folders 129 | @echo "+ $@" 130 | @rm -f .env 131 | @rm -rf ./dist 132 | @rm -rf ./tmp 133 | @rm -rf ./node_modules 134 | @rm -rf ./.pnpm-store 135 | @rm -rf ./backend/out 136 | 137 | uninstall-plugin: ## Uninstall plugin from steamdeck, restart Decky 138 | @echo "+ $@" 139 | @ssh -t $(DECK_USER)@$(DECK_HOST) -p $(DECK_PORT) -i $(DECK_KEY) \ 140 | "sudo sh -c 'rm -rf $(DECK_HOME)/homebrew/plugins/$(PLUGIN_FOLDER)/ && systemctl restart plugin_loader.service'" 141 | @echo -e '\033[0;32m+ all is good, restarting Decky...\033[0m' 142 | 143 | docker-rebuild-image: ## Rebuild docker image 144 | @echo "+ $@" 145 | @docker compose build --pull 146 | 147 | docker-build: ## Build project inside docker container 148 | @$(MAKE) build-back 149 | @echo "+ $@" 150 | @docker run --rm -i -v $(PWD):/plugin -v $(PWD)/tmp/out:/out ghcr.io/steamdeckhomebrew/builder:latest -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English](./README_en.md) | 简体中文 2 | 3 | # PowerControl 4 | 5 | [![](https://img.shields.io/github/downloads/mengmeet/PowerControl/total.svg)](https://gitHub.com/mengmeet/PowerControl/releases) [![](https://img.shields.io/github/downloads/mengmeet/PowerControl/latest/total)](https://github.com/mengmeet/PowerControl/releases/latest) [![](https://img.shields.io/github/v/release/mengmeet/PowerControl)](https://github.com/mengmeet/PowerControl/releases/latest) 6 | 7 | 用于 [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader) 的插件 8 | 为手持设备提供性能设置调整 9 | 10 | ## 一键安装 11 | ``` 12 | curl -L https://raw.githubusercontent.com/mengmeet/PowerControl/main/install.sh | sh 13 | ``` 14 | 15 | ## 手动安装 16 | 17 | 1. 安装 [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader) 18 | 2. 下载 [Releases](https://github.com/Gawah/PowerControl/releases) 页面的PowerControl.tar.gz 19 | 3. 调整插件目录权限 `chmod -R 777 ${HOME}/homebrew/plugins` 20 | 4. 解压到/home/xxxx/homebrew/plugins/下 21 | 5. 重启 decky-loader, `sudo systemctl restart plugin_loader.service` 22 | 6. 进入游戏模式,即可在decky页面使用该插件 23 | 24 | 25 | ## 功能 26 | 1. 开关睿频 27 | 2. 开关超线程 28 | 3. 调整物理核心开启数量 29 | 4. 限制TDP 30 | 5. 固定GPU频率 31 | 6. 自动GPU频率 32 | 7. 风扇控制 33 | 34 | ## 支持设备 35 | - 大多数 AMD Ryzen 处理器 36 | - Intel 处理器 (实验, 使用GPD WIN3 测试) 37 | 38 | 39 | 54 | 55 | 76 | 77 | 78 | 79 | ## 支持 80 | 可以加入我们的qq群:487945399反馈问题,或者在[issues](https://github.com/Gawah/PowerControl/issues)提交 81 | 82 | ## Reference 83 | [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader) 84 | [vibrantDeck](https://github.com/libvibrant/vibrantDeck) 85 | [decky-plugin-template](https://github.com/SteamDeckHomebrew/decky-plugin-template) 86 | [RyzenAdj](https://github.com/FlyGoat/RyzenAdj) 87 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./README.md) 2 | 3 | # PowerControl 4 | 5 | [![](https://img.shields.io/github/downloads/mengmeet/PowerControl/total.svg)](https://gitHub.com/mengmeet/PowerControl/releases) [![](https://img.shields.io/github/downloads/mengmeet/PowerControl/latest/total)](https://github.com/mengmeet/PowerControl/releases/latest) [![](https://img.shields.io/github/v/release/mengmeet/PowerControl)](https://github.com/mengmeet/PowerControl/releases/latest) 6 | 7 | Plugin for [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader) 8 | Provide performance settings adjustments for handheld 9 | 10 | ## Manual installation 11 | 12 | 1. Install [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader) 13 | 2. Download PowerControl.tar.gz from [Releases](https://github.com/Gawah/PowerControl/releases) 14 | 3. Modify directory permissions `chmod -R 777 ${HOME}/homebrew/plugins` 15 | 4. Extract to /home/xxxx/homebrew/plugins/ 16 | 5. Restart decky-loader, `sudo systemctl restart plugin_loader.service` 17 | 18 | ## Automatic installation 19 | ``` 20 | curl -L https://raw.githubusercontent.com/mengmeet/PowerControl/main/install.sh | sh 21 | ``` 22 | 23 | ## Function 24 | 1. CPU Boost 25 | 2. SMT 26 | 3. Set the number of cores to enable 27 | 4. TDP 28 | 5. GPU frequency 29 | 6. Auto GPU frequency 30 | 7. Fan control 31 | 32 | ## Supported devices 33 | - Most AMD Ryzen CPU 34 | - Some Intel CPU (experimental, tested with GPD WIN3) 35 | 36 | ## Reference 37 | [decky-loader](https://github.com/SteamDeckHomebrew/decky-loader) 38 | [vibrantDeck](https://github.com/libvibrant/vibrantDeck) 39 | [decky-plugin-template](https://github.com/SteamDeckHomebrew/decky-plugin-template) 40 | [RyzenAdj](https://github.com/FlyGoat/RyzenAdj) 41 | -------------------------------------------------------------------------------- /assets/20230107182755_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengmeet/PowerControl/0157e2e41d63d8e308e16abd6244df94cc68fdb8/assets/20230107182755_1.jpg -------------------------------------------------------------------------------- /assets/20230107182852_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengmeet/PowerControl/0157e2e41d63d8e308e16abd6244df94cc68fdb8/assets/20230107182852_1.jpg -------------------------------------------------------------------------------- /assets/20230108234559_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengmeet/PowerControl/0157e2e41d63d8e308e16abd6244df94cc68fdb8/assets/20230108234559_1.jpg -------------------------------------------------------------------------------- /assets/20230110153822_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengmeet/PowerControl/0157e2e41d63d8e308e16abd6244df94cc68fdb8/assets/20230110153822_1.jpg -------------------------------------------------------------------------------- /assets/20230111054307_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengmeet/PowerControl/0157e2e41d63d8e308e16abd6244df94cc68fdb8/assets/20230111054307_1.jpg -------------------------------------------------------------------------------- /assets/20230319145542_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengmeet/PowerControl/0157e2e41d63d8e308e16abd6244df94cc68fdb8/assets/20230319145542_1.jpg -------------------------------------------------------------------------------- /backend/sh_tools.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function get_gpu_device() 4 | { 5 | for gpu_device in /sys/class/drm/card?/device; do 6 | if [ -d "$gpu_device" ]; then 7 | echo "$gpu_device" 8 | return 9 | fi 10 | done 11 | } 12 | 13 | gpu_device=$(get_gpu_device) 14 | 15 | # 弃用 16 | function set_cpu_Freq() 17 | { 18 | cpu_index=$1 19 | let cpu_freq=$2 20 | if(($cpu_index==0));then 21 | cpu_isOnLine=1 22 | else 23 | cpu_isOnLine=$(cat /sys/devices/system/cpu/cpu${cpu_index}/online) 24 | fi 25 | if(($cpu_freq==0));then 26 | if((cpu_isOnLine==0));then 27 | sudo echo 1 > "/sys/devices/system/cpu/cpu${cpu_index}/online" 28 | sudo echo "schedutil" > "/sys/devices/system/cpu/cpu${cpu_index}/cpufreq/scaling_governor" 29 | sudo echo 0 > "/sys/devices/system/cpu/cpu${cpu_index}/online" 30 | else 31 | sudo echo "schedutil" > "/sys/devices/system/cpu/cpu${cpu_index}/cpufreq/scaling_governor" 32 | fi 33 | else 34 | if((cpu_isOnLine==0));then 35 | sudo echo 1 > "/sys/devices/system/cpu/cpu${cpu_index}/online" 36 | sudo echo "userspace" > "/sys/devices/system/cpu/cpu${cpu_index}/cpufreq/scaling_governor" 37 | sudo echo $cpu_freq > "/sys/devices/system/cpu/cpu${cpu_index}/cpufreq/scaling_max_freq" 38 | sudo echo 0 > "/sys/devices/system/cpu/cpu${cpu_index}/online" 39 | else 40 | sudo echo "userspace" > "/sys/devices/system/cpu/cpu${cpu_index}/cpufreq/scaling_governor" 41 | sudo echo $cpu_freq > "/sys/devices/system/cpu/cpu${cpu_index}/cpufreq/scaling_max_freq" 42 | fi 43 | fi 44 | } 45 | 46 | # not need 47 | function get_cpu_nowFreq() 48 | { 49 | cpu_index=$1 50 | echo "$(cat /sys/devices/system/cpu/cpufreq/policy${cpu_index}/scaling_cur_freq)" 51 | } 52 | 53 | # 弃用 54 | function get_cpu_AvailableFreq() 55 | { 56 | echo "$(cat /sys/devices/system/cpu/cpufreq/policy0/scaling_available_frequencies)" 57 | } 58 | 59 | # not need 60 | function set_cpu_online() 61 | { 62 | cpu_index=$1 63 | cpu_online=$2 64 | echo $cpu_online > "/sys/devices/system/cpu/cpu${cpu_index}/online" 65 | } 66 | 67 | # not need 68 | function get_cpuID() 69 | { 70 | cat /proc/cpuinfo | grep "model name" | sed -n '1p'| cut -d : -f 2 | xargs 71 | } 72 | 73 | # not need 74 | function set_cpu_tdp() 75 | { 76 | ryzenadj_path=$1 77 | let slow=$2*1000 78 | let fast=$3*1000 79 | sudo $ryzenadj_path --stapm-limit=$fast --fast-limit=$fast --slow-limit=$fast --tctl-temp=90 80 | sudo echo "${ryzenadj_path} --stapm-limit=${fast} --fast-limit=${fast} --slow-limit=${slow}" >> /tmp/powerControl_sh.log 81 | } 82 | 83 | # not need 84 | function set_clock_limits() 85 | { 86 | let min=$1 87 | let max=$2 88 | if(($min==0 || $max==0));then 89 | sudo echo "auto" > "${gpu_device}/power_dpm_force_performance_level" 90 | else 91 | sudo echo "manual" > "${gpu_device}/power_dpm_force_performance_level" 92 | sudo echo "s 0 ${min}" > "${gpu_device}/pp_od_clk_voltage" 93 | sudo echo "s 1 ${max}" > "${gpu_device}/pp_od_clk_voltage" 94 | sudo echo "c" > "${gpu_device}/pp_od_clk_voltage" 95 | fi 96 | sudo echo "gpu_clock_limit "$1 $2 >> /tmp/powerControl_sh.log 97 | } 98 | 99 | # not need 100 | function get_gpuFreqMin() 101 | { 102 | sudo echo "manual"> "${gpu_device}/power_dpm_force_performance_level" 103 | echo "$(sudo cat ${gpu_device}/pp_od_clk_voltage|grep -a "SCLK:"|awk '{print $2}'|sed -e 's/Mhz//g'|xargs)" 104 | } 105 | 106 | # not need 107 | function get_gpuFreqMax() 108 | { 109 | sudo echo "manual" > "${gpu_device}/power_dpm_force_performance_level" 110 | echo "$(sudo cat ${gpu_device}/pp_od_clk_voltage|grep -a "SCLK:"|awk '{print $3}'|sed -e 's/Mhz//g'|xargs)" 111 | } 112 | 113 | # 忽略 114 | function set_gpu_flk() 115 | { 116 | flk=$1 117 | index=$(((1600-$flk)/400)) 118 | now_mode=$(cat ${gpu_device}/power_dpm_force_performance_level) 119 | if [[ "$now_mode"!="manual" ]];then 120 | sudo echo "manual" > "${gpu_device}/power_dpm_force_performance_level" 121 | sudo echo "$index" > "${gpu_device}/pp_dpm_fclk" 122 | else 123 | sudo echo "$index" > "${gpu_device}/pp_dpm_fclk" 124 | fi 125 | sudo echo "gpu_flk_limit " $index >> /tmp/powerControl_sh.log 126 | } 127 | 128 | # 忽略 129 | function check_clock_limits() 130 | { 131 | mode=$1 132 | now_mode=$(cat ${gpu_device}/power_dpm_force_performance_level) 133 | if [[ "$now_mode"!="$mode" ]];then 134 | if(( "$1" == "manual"));then 135 | sudo echo "manual" > "${gpu_device}/power_dpm_force_performance_level" 136 | sudo echo "s 0 $2" > "${gpu_device}/pp_od_clk_voltage" 137 | sudo echo "s 1 $3" > "${gpu_device}/pp_od_clk_voltage" 138 | sudo echo "c" > "${gpu_device}/pp_od_clk_voltage" 139 | else 140 | sudo echo "auto" > "${gpu_device}/power_dpm_force_performance_level" 141 | fi 142 | fi 143 | } 144 | 145 | # not need 146 | function set_cpu_boost() 147 | { 148 | boost=$1 149 | boost_path="/sys/devices/system/cpu/cpufreq/boost" 150 | amd_pstate_path="/sys/devices/system/cpu/amd_pstate/status" 151 | 152 | 153 | if [ -f "${amd_pstate_path}" ]; then 154 | echo "disable" > "${amd_pstate_path}" 155 | # modprobe -r amd_pstate_ut 156 | modprobe acpi_cpufreq 157 | fi 158 | 159 | if (($boost == 1)); then 160 | echo 1 > "${boost_path}" 161 | else 162 | echo 1 > "${boost_path}" 163 | echo 0 > "${boost_path}" 164 | fi 165 | } 166 | 167 | # not need 168 | function get_language() 169 | { 170 | language_path=$1 171 | sudo cat $language_path|grep language|sed -n '1p'|xargs|cut -d " " -f 2 172 | } 173 | 174 | 175 | if [ -n "$1" ]; then 176 | case "$1" in 177 | set_cpu_online)set_cpu_online $2 $3;; 178 | set_cpu_Freq)set_cpu_Freq $2 $3;; 179 | get_cpu_nowFreq)get_cpu_nowFreq $2;; 180 | get_cpu_AvailableFreq)get_cpu_AvailableFreq $2 $3;; 181 | set_cpu_tdp)set_cpu_tdp $2 $3 $4;; 182 | set_clock_limits)set_clock_limits $2 $3;; 183 | set_cpu_boost)set_cpu_boost $2;; 184 | check_clock_limits)check_clock_limits $2 $3 $4;; 185 | set_gpu_flk)set_gpu_flk $2;; 186 | get_cpuID)get_cpuID;; 187 | get_language)get_language $2;; 188 | get_gpuFreqMin)get_gpuFreqMin;; 189 | get_gpuFreqMax)get_gpuFreqMax;; 190 | *)sudo echo $1 $2 $3 $4>> /tmp/powerControl_sh.log;; 191 | esac 192 | fi 193 | -------------------------------------------------------------------------------- /bin/ryzenadj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengmeet/PowerControl/0157e2e41d63d8e308e16abd6244df94cc68fdb8/bin/ryzenadj -------------------------------------------------------------------------------- /decky.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | This module exposes various constants and helpers useful for decky plugins. 3 | 4 | * Plugin's settings and configurations should be stored under `DECKY_PLUGIN_SETTINGS_DIR`. 5 | * Plugin's runtime data should be stored under `DECKY_PLUGIN_RUNTIME_DIR`. 6 | * Plugin's persistent log files should be stored under `DECKY_PLUGIN_LOG_DIR`. 7 | 8 | Avoid writing outside of `DECKY_HOME`, storing under the suggested paths is strongly recommended. 9 | 10 | Some basic migration helpers are available: `migrate_any`, `migrate_settings`, `migrate_runtime`, `migrate_logs`. 11 | 12 | A logging facility `logger` is available which writes to the recommended location. 13 | """ 14 | 15 | __version__ = '1.0.0' 16 | 17 | import logging 18 | 19 | from typing import Any 20 | 21 | """ 22 | Constants 23 | """ 24 | 25 | HOME: str 26 | """ 27 | The home directory of the effective user running the process. 28 | Environment variable: `HOME`. 29 | If `root` was specified in the plugin's flags it will be `/root` otherwise the user whose home decky resides in. 30 | e.g.: `/home/deck` 31 | """ 32 | 33 | USER: str 34 | """ 35 | The effective username running the process. 36 | Environment variable: `USER`. 37 | It would be `root` if `root` was specified in the plugin's flags otherwise the user whose home decky resides in. 38 | e.g.: `deck` 39 | """ 40 | 41 | DECKY_VERSION: str 42 | """ 43 | The version of the decky loader. 44 | Environment variable: `DECKY_VERSION`. 45 | e.g.: `v2.5.0-pre1` 46 | """ 47 | 48 | DECKY_USER: str 49 | """ 50 | The user whose home decky resides in. 51 | Environment variable: `DECKY_USER`. 52 | e.g.: `deck` 53 | """ 54 | 55 | DECKY_USER_HOME: str 56 | """ 57 | The home of the user where decky resides in. 58 | Environment variable: `DECKY_USER_HOME`. 59 | e.g.: `/home/deck` 60 | """ 61 | 62 | DECKY_HOME: str 63 | """ 64 | The root of the decky folder. 65 | Environment variable: `DECKY_HOME`. 66 | e.g.: `/home/deck/homebrew` 67 | """ 68 | 69 | DECKY_PLUGIN_SETTINGS_DIR: str 70 | """ 71 | The recommended path in which to store configuration files (created automatically). 72 | Environment variable: `DECKY_PLUGIN_SETTINGS_DIR`. 73 | e.g.: `/home/deck/homebrew/settings/decky-plugin-template` 74 | """ 75 | 76 | DECKY_PLUGIN_RUNTIME_DIR: str 77 | """ 78 | The recommended path in which to store runtime data (created automatically). 79 | Environment variable: `DECKY_PLUGIN_RUNTIME_DIR`. 80 | e.g.: `/home/deck/homebrew/data/decky-plugin-template` 81 | """ 82 | 83 | DECKY_PLUGIN_LOG_DIR: str 84 | """ 85 | The recommended path in which to store persistent logs (created automatically). 86 | Environment variable: `DECKY_PLUGIN_LOG_DIR`. 87 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template` 88 | """ 89 | 90 | DECKY_PLUGIN_DIR: str 91 | """ 92 | The root of the plugin's directory. 93 | Environment variable: `DECKY_PLUGIN_DIR`. 94 | e.g.: `/home/deck/homebrew/plugins/decky-plugin-template` 95 | """ 96 | 97 | DECKY_PLUGIN_NAME: str 98 | """ 99 | The name of the plugin as specified in the 'plugin.json'. 100 | Environment variable: `DECKY_PLUGIN_NAME`. 101 | e.g.: `Example Plugin` 102 | """ 103 | 104 | DECKY_PLUGIN_VERSION: str 105 | """ 106 | The version of the plugin as specified in the 'package.json'. 107 | Environment variable: `DECKY_PLUGIN_VERSION`. 108 | e.g.: `0.0.1` 109 | """ 110 | 111 | DECKY_PLUGIN_AUTHOR: str 112 | """ 113 | The author of the plugin as specified in the 'plugin.json'. 114 | Environment variable: `DECKY_PLUGIN_AUTHOR`. 115 | e.g.: `John Doe` 116 | """ 117 | 118 | DECKY_PLUGIN_LOG: str 119 | """ 120 | The path to the plugin's main logfile. 121 | Environment variable: `DECKY_PLUGIN_LOG`. 122 | e.g.: `/home/deck/homebrew/logs/decky-plugin-template/plugin.log` 123 | """ 124 | 125 | """ 126 | Migration helpers 127 | """ 128 | 129 | 130 | def migrate_any(target_dir: str, *files_or_directories: str) -> dict[str, str]: 131 | """ 132 | Migrate files and directories to a new location and remove old locations. 133 | Specified files will be migrated to `target_dir`. 134 | Specified directories will have their contents recursively migrated to `target_dir`. 135 | 136 | Returns the mapping of old -> new location. 137 | """ 138 | 139 | 140 | def migrate_settings(*files_or_directories: str) -> dict[str, str]: 141 | """ 142 | Migrate files and directories relating to plugin settings to the recommended location and remove old locations. 143 | Specified files will be migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 144 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_SETTINGS_DIR`. 145 | 146 | Returns the mapping of old -> new location. 147 | """ 148 | 149 | 150 | def migrate_runtime(*files_or_directories: str) -> dict[str, str]: 151 | """ 152 | Migrate files and directories relating to plugin runtime data to the recommended location and remove old locations 153 | Specified files will be migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 154 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_RUNTIME_DIR`. 155 | 156 | Returns the mapping of old -> new location. 157 | """ 158 | 159 | 160 | def migrate_logs(*files_or_directories: str) -> dict[str, str]: 161 | """ 162 | Migrate files and directories relating to plugin logs to the recommended location and remove old locations. 163 | Specified files will be migrated to `DECKY_PLUGIN_LOG_DIR`. 164 | Specified directories will have their contents recursively migrated to `DECKY_PLUGIN_LOG_DIR`. 165 | 166 | Returns the mapping of old -> new location. 167 | """ 168 | 169 | 170 | """ 171 | Logging 172 | """ 173 | 174 | logger: logging.Logger 175 | """The main plugin logger writing to `DECKY_PLUGIN_LOG`.""" 176 | 177 | """ 178 | Event handling 179 | """ 180 | # TODO better docstring im lazy 181 | async def emit(event: str, *args: Any) -> None: 182 | """ 183 | Send an event to the frontend. 184 | """ -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | 3 | set -e 4 | 5 | if [ "$EUID" -eq 0 ]; then 6 | echo "Please do not run as root" 7 | exit 8 | fi 9 | 10 | github_api_url="https://api.github.com/repos/mengmeet/PowerControl/releases/latest" 11 | package="PowerControl" 12 | 13 | echo "installing $package" 14 | 15 | temp=$(mktemp -d) 16 | 17 | chmod -R +w "${HOME}/homebrew/plugins/" 18 | plugin_dir="${HOME}/homebrew/plugins/${package}" 19 | mkdir -p $plugin_dir 20 | 21 | use_jq=false 22 | if [ -x "$(command -v jq)" ]; then 23 | use_jq=true 24 | fi 25 | 26 | RELEASE=$(curl -s "$github_api_url") 27 | 28 | if [[ $use_jq == true ]]; then 29 | echo "Using jq" 30 | MESSAGE=$(echo "$RELEASE" | jq -r '.message') 31 | RELEASE_VERSION=$(echo "$RELEASE" | jq -r '.tag_name') 32 | RELEASE_URL=$(echo "$RELEASE" | jq -r '.assets[0].browser_download_url') 33 | else 34 | MESSAGE=$(echo $RELEASE | grep "message" | cut -d '"' -f 4) 35 | RELEASE_URL=$(echo $RELEASE | grep "browser_download_url" | cut -d '"' -f 4 | grep ".tar.gz") 36 | RELEASE_VERSION=$(echo $RELEASE | grep "tag_name" | cut -d '"' -f 4) 37 | fi 38 | 39 | if [[ "$MESSAGE" != "null" ]]; then 40 | echo "error: $MESSAGE" >&2 41 | exit 1 42 | fi 43 | 44 | if [ -z "$RELEASE_URL" ]; then 45 | echo "Failed to get latest release" >&2 46 | exit 1 47 | fi 48 | 49 | temp_file="${temp}/${package}.tar.gz" 50 | 51 | echo "Downloading $package $RELEASE_VERSION" 52 | curl -L "$RELEASE_URL" -o "$temp_file" 53 | 54 | sudo tar -xzf "$temp_file" -C $temp 55 | sudo rsync -av "${temp}/${package}/" $plugin_dir --delete 56 | 57 | rm "$temp_file" 58 | sudo systemctl restart plugin_loader.service 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "power_control", 3 | "version": "2.19.0", 4 | "description": "PowerControl plugin.", 5 | "type": "module", 6 | "scripts": { 7 | "build": "shx rm -rf dist && rollup -c", 8 | "watch": "rollup -c -w", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/mengmeet/PowerControl.git" 14 | }, 15 | "keywords": [ 16 | "decky", 17 | "plugin", 18 | "plugin-template", 19 | "steam-deck", 20 | "deck" 21 | ], 22 | "author": "yxx", 23 | "license": "BSD-3-Clause", 24 | "bugs": { 25 | "url": "https://github.com/mengmeet/PowerControl/issues" 26 | }, 27 | "homepage": "https://github.com/mengmeet/PowerControl#readme", 28 | "devDependencies": { 29 | "@decky/rollup": "^1.0.1", 30 | "@decky/ui": "^4.10.0", 31 | "@types/markdown-it": "^14.1.2", 32 | "@types/react": "19.1.1", 33 | "@types/react-dom": "19.1.2", 34 | "@types/webpack": "^5.28.5", 35 | "markdown-it": "^14.1.0", 36 | "merge-anything": "^6.0.6", 37 | "rollup": "^4.40.0", 38 | "shx": "^0.4.0", 39 | "tslib": "^2.8.1", 40 | "typescript": "^5.8.3" 41 | }, 42 | "dependencies": { 43 | "@decky/api": "^1.1.2", 44 | "i18next": "^24.2.3", 45 | "react-icons": "^5.5.0", 46 | "typescript-json-serializer": "^6.0.1" 47 | }, 48 | "pnpm": { 49 | "peerDependencyRules": { 50 | "ignoreMissing": [ 51 | "react", 52 | "react-dom" 53 | ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PowerControl", 3 | "author": "yxx", 4 | "flags": ["root"], 5 | "api_version": 1, 6 | "publish": { 7 | "tags": ["root"], 8 | "description": "PowerControl plugin.", 9 | "image": "https://opengraph.githubassets.com/1/SteamDeckHomebrew/PluginLoader" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /py_modules/conf_manager.py: -------------------------------------------------------------------------------- 1 | import decky 2 | from config import CONFIG_KEY 3 | from settings import SettingsManager 4 | 5 | 6 | class ConfManager: 7 | def __init__(self): 8 | self.sysSettings = SettingsManager( 9 | name="config", settings_directory=decky.DECKY_PLUGIN_SETTINGS_DIR 10 | ) 11 | self.fansSettings = SettingsManager( 12 | name="fans_config", 13 | settings_directory=decky.DECKY_PLUGIN_SETTINGS_DIR, 14 | ) 15 | 16 | def getSettings(self): 17 | return self.sysSettings.getSetting(CONFIG_KEY) or {} 18 | 19 | def setSettings(self, settings): 20 | self.sysSettings.setSetting(CONFIG_KEY, settings) 21 | return True 22 | 23 | def getFanSettings(self): 24 | return self.fansSettings.getSetting(CONFIG_KEY) or {} 25 | 26 | def setFanSettingsByKey(self, key, value): 27 | self.fansSettings.setSetting(key, value) 28 | return True 29 | 30 | 31 | confManager = ConfManager() 32 | -------------------------------------------------------------------------------- /py_modules/devices/README.md: -------------------------------------------------------------------------------- 1 | # Devices 2 | 3 | ## AYANEO devices supports bypass power supply ec version 4 | Air1S 8.4.0.0.27 5 | Air 3.1.0.4.78 6 | AirPlus 1B 7 | Slide 1B 8 | AirPlusMendocino 7.0.0.0.13 9 | AirPlusIntel 0B (?) 10 | FlipKB 8.7.0.0.33 11 | FlipDS 8.7.0.0.33 12 | Kun 8.3.0.0.63 13 | -------------------------------------------------------------------------------- /py_modules/devices/__init__.py: -------------------------------------------------------------------------------- 1 | from .asus.asus_device import AsusDevice 2 | from .ayaneo.ayaneo_air import AyaneoAir 3 | from .ayaneo.ayaneo_air_1s import AyaneoAir1S 4 | from .ayaneo.ayaneo_air_plus import AyaneoAirPlus 5 | from .ayaneo.ayaneo_air_plus_intel import AyaneoAirPlusIntel 6 | from .ayaneo.ayaneo_air_plus_mendocino import AyaneoAirPlusMendocino 7 | from .ayaneo.ayaneo_device import AyaneoDevice 8 | from .ayaneo.ayaneo_device_ii import AyaneoDeviceII 9 | from .ayaneo.ayaneo_flip_ds import AyaneoFlipDS 10 | from .ayaneo.ayaneo_flip_kb import AyaneoFlipKB 11 | from .ayaneo.ayaneo_kun import AyaneoKun 12 | from .idevice import IDevice 13 | from .lenovo.lenovo_device import LenovoDevice 14 | from .msi.msi_claw8 import MsiClaw8 15 | from .msi.msi_claw_a1m import MsiClawA1M 16 | from .msi.msi_device import MsiDevice 17 | from .power_device import PowerDevice 18 | 19 | __all__ = [ 20 | "AyaneoAir", 21 | "AyaneoAir1S", 22 | "AyaneoAirPlus", 23 | "AyaneoAirPlusIntel", 24 | "AyaneoAirPlusMendocino", 25 | "AyaneoDevice", 26 | "AyaneoDeviceII", 27 | "AyaneoFlipDS", 28 | "AyaneoFlipKB", 29 | "AyaneoKun", 30 | "IDevice", 31 | "MsiClaw8", 32 | "MsiClawA1M", 33 | "MsiDevice", 34 | "PowerDevice", 35 | "AsusDevice", 36 | "LenovoDevice", 37 | ] 38 | -------------------------------------------------------------------------------- /py_modules/devices/asus/asus_device.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | 4 | from config import logger 5 | 6 | from ..firmware_attribute_device import FirmwareAttributeDevice 7 | 8 | PLATFORM_PROFILE_PATH = "/sys/firmware/acpi/platform_profile" 9 | 10 | LEGACY_WMI_PATH = "/sys/devices/platform/asus-nb-wmi" 11 | 12 | FAST_WMI_PATH = f"{LEGACY_WMI_PATH}/ppt_fppt" 13 | SLOW_WMI_PATH = f"{LEGACY_WMI_PATH}/ppt_pl2_sppt" 14 | STAPM_WMI_PATH = f"{LEGACY_WMI_PATH}/ppt_pl1_spl" 15 | 16 | 17 | ATTRIBUTE_NAME = "asus-armoury" 18 | PLATFORM_PROFILE_NAME = "asus-wmi" 19 | SUGGESTED_DEFAULT = ["custom", "performance"] 20 | 21 | 22 | # credit: https://github.com/aarron-lee/SimpleDeckyTDP/blob/main/py_modules/devices/rog_ally.py 23 | class AsusDevice(FirmwareAttributeDevice): 24 | def __init__(self) -> None: 25 | super().__init__() 26 | self.init_attribute(ATTRIBUTE_NAME, PLATFORM_PROFILE_NAME) 27 | 28 | def set_tdp(self, tdp: int) -> None: 29 | logger.debug(f"Setting TDP to {tdp}") 30 | if self.supports_attribute_tdp(): 31 | super().set_tdp(tdp) 32 | elif self._supports_wmi_tdp(): 33 | logger.debug(f"Setting TDP to {tdp} by ASUS WMI") 34 | self._set_stapm(tdp) 35 | self._set_slow(tdp) 36 | self._set_fast(tdp) 37 | else: 38 | super().fallback_set_tdp(tdp) 39 | 40 | def _set_stapm(self, stapm: int) -> None: 41 | logger.debug(f"Setting STAPM to {stapm}") 42 | if os.path.exists(STAPM_WMI_PATH): 43 | with open(STAPM_WMI_PATH, "w") as f: 44 | f.write(str(stapm)) 45 | sleep(0.1) 46 | 47 | def _set_slow(self, slow: int) -> None: 48 | logger.debug(f"Setting SLOW to {slow}") 49 | if os.path.exists(SLOW_WMI_PATH): 50 | with open(SLOW_WMI_PATH, "w") as f: 51 | f.write(str(slow)) 52 | sleep(0.1) 53 | 54 | def _set_fast(self, fast: int) -> None: 55 | logger.debug(f"Setting FAST to {fast}") 56 | if os.path.exists(FAST_WMI_PATH): 57 | with open(FAST_WMI_PATH, "w") as f: 58 | f.write(str(fast)) 59 | sleep(0.1) 60 | 61 | def _supports_wmi_tdp(self) -> bool: 62 | return ( 63 | os.path.exists(FAST_WMI_PATH) 64 | and os.path.exists(SLOW_WMI_PATH) 65 | and os.path.exists(STAPM_WMI_PATH) 66 | ) 67 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_air.py: -------------------------------------------------------------------------------- 1 | from .ayaneo_device import AyaneoDevice 2 | 3 | 4 | class AyaneoAir(AyaneoDevice): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | # 3.1.0.4.78 or later 8 | self.ec_version_of_bypass_charge = [3, 1, 0, 4, 78] 9 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_air_1s.py: -------------------------------------------------------------------------------- 1 | from .ayaneo_air import AyaneoAir 2 | 3 | 4 | class AyaneoAir1S(AyaneoAir): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | # 8.4.0.0.27 or later 8 | self.ec_version_of_bypass_charge = [8, 4, 0, 0, 27] 9 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_air_plus.py: -------------------------------------------------------------------------------- 1 | from .ayaneo_device_ii import AyaneoDeviceII 2 | 3 | 4 | class AyaneoAirPlus(AyaneoDeviceII): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | self.ec_version_of_bypass_charge = [0, 0x1B, 0, 0, 0] 8 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_air_plus_intel.py: -------------------------------------------------------------------------------- 1 | from .ayaneo_air_plus import AyaneoAirPlus 2 | 3 | 4 | class AyaneoAirPlusIntel(AyaneoAirPlus): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | # 8 | self.ec_version_of_bypass_charge = 0x0B 9 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_air_plus_mendocino.py: -------------------------------------------------------------------------------- 1 | from .ayaneo_air_plus import AyaneoAirPlus 2 | 3 | 4 | class AyaneoAirPlusMendocino(AyaneoAirPlus): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | # 7.0.0.0.13 or later 8 | self.ec_version_of_bypass_charge = [7, 0, 0, 0, 13] 9 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_device_ii.py: -------------------------------------------------------------------------------- 1 | from config import logger 2 | from ec import EC 3 | 4 | from .ayaneo_device import AyaneoDevice 5 | 6 | EC_BYPASS_CHARGE_ADDR = 0xD1 7 | EC_BYPASS_CHARGE_OPEN = 0x01 8 | EC_BYPASS_CHARGE_CLOSE = 0x65 9 | EC_COMM_PORT = 0x4E 10 | EC_DATA_PORT = 0x4F 11 | 12 | 13 | # from https://github.com/Valkirie/HandheldCompanion/blob/main/HandheldCompanion/Devices/AYANEO/AYANEODeviceCEii.cs 14 | class AyaneoDeviceII(AyaneoDevice): 15 | def __init__(self) -> None: 16 | super().__init__() 17 | self.ec_bypass_charge_addr = EC_BYPASS_CHARGE_ADDR 18 | self.ec_bypass_charge_open = EC_BYPASS_CHARGE_OPEN 19 | self.ec_bypass_charge_close = EC_BYPASS_CHARGE_CLOSE 20 | self.ec_comm_port = EC_COMM_PORT 21 | self.ec_data_port = EC_DATA_PORT 22 | 23 | def _ec_ram_direct_read(self, address: int, offset=0xD1) -> int: 24 | address2 = address | (offset << 8) 25 | return EC.RamRead(self.ec_comm_port, self.ec_data_port, address2) 26 | 27 | def _ec_ram_direct_write(self, address: int, data: int, offset=0xD1) -> None: 28 | address2 = address | (offset << 8) 29 | logger.debug( 30 | f"Directly writing to EC RAM: address={hex(address2)} data={hex(data)}" 31 | ) 32 | EC.RamWrite(self.ec_comm_port, self.ec_data_port, address2, data) 33 | 34 | def supports_charge_limit(self) -> bool: 35 | return self.supports_bypass_charge() 36 | 37 | def get_bypass_charge(self) -> bool: 38 | """ 39 | Get the status of the bypass charge switch 40 | :return: 41 | """ 42 | value = self._ec_ram_direct_read(self.ec_bypass_charge_addr) 43 | logger.debug(f"Bypass charge status: {hex(value)}") 44 | return value == self.ec_bypass_charge_open 45 | 46 | def _set_bypass_charge(self, value: bool) -> None: 47 | """ 48 | Set the status of the bypass charge switch 49 | :param value: 50 | :return: 51 | """ 52 | current_value = self._ec_ram_direct_read(self.ec_bypass_charge_addr) 53 | write_value = ( 54 | self.ec_bypass_charge_open if value else self.ec_bypass_charge_close 55 | ) 56 | logger.info( 57 | f">>>> Setting bypass charge: {value}, current: {hex(current_value)}, write: {hex(write_value)}" 58 | ) 59 | 60 | if current_value != write_value: 61 | self._ec_ram_direct_write(self.ec_bypass_charge_addr, write_value) 62 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_flip_ds.py: -------------------------------------------------------------------------------- 1 | from .ayaneo_flip_kb import AyaneoFlipKB 2 | 3 | 4 | class AyaneoFlipDS(AyaneoFlipKB): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | # 8.7.0.0.33 or later 8 | self.ec_version_of_bypass_charge = [8, 7, 0, 0, 33] 9 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_flip_kb.py: -------------------------------------------------------------------------------- 1 | from .ayaneo_device import AyaneoDevice 2 | 3 | 4 | class AyaneoFlipKB(AyaneoDevice): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | # 8.7.0.0.33 or later 8 | self.ec_version_of_bypass_charge = [8, 7, 0, 0, 33] 9 | -------------------------------------------------------------------------------- /py_modules/devices/ayaneo/ayaneo_kun.py: -------------------------------------------------------------------------------- 1 | from .ayaneo_device import AyaneoDevice 2 | 3 | 4 | class AyaneoKun(AyaneoDevice): 5 | def __init__(self) -> None: 6 | super().__init__() 7 | # 8.3.0.0.63 or later 8 | self.ec_version_of_bypass_charge = [8, 3, 0, 0, 63] 9 | -------------------------------------------------------------------------------- /py_modules/devices/firmware_attribute_device.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | from .power_device import PowerDevice 4 | from config import logger 5 | 6 | PREFIX = "/sys/class/firmware-attributes" 7 | SPL_SUFFIX = "ppt_pl1_spl" 8 | SLOW_SUFFIX = "ppt_pl2_sppt" 9 | FAST_SUFFIX = "ppt_pl3_fppt" 10 | 11 | SUGGESTED_DEFAULT = ["custom", "performance"] 12 | 13 | 14 | class FirmwareAttributeDevice(PowerDevice): 15 | def __init__(self): 16 | super().__init__() 17 | self.attribute = None 18 | self.profile_name = None 19 | 20 | def init_attribute(self, attribute: str, profile_name: str) -> None: 21 | self.attribute = attribute 22 | self.profile_name = profile_name 23 | 24 | def supports_attribute_tdp(self) -> bool: 25 | if not self.check_init(): 26 | return False 27 | return os.path.exists(f"{PREFIX}/{self.attribute}/attributes/{SPL_SUFFIX}") 28 | 29 | def check_init(self) -> bool: 30 | if self.attribute is not None and self.profile_name is not None: 31 | return True 32 | logger.error("Attribute or profile name is not set") 33 | return False 34 | 35 | def set_tdp(self, tdp: int) -> None: 36 | logger.info(f"Setting TDP to {tdp}") 37 | if not self.supports_attribute_tdp(): 38 | logger.info("Device does not support attribute TDP, use fallback method") 39 | return self.fallback_set_tdp(tdp) 40 | base_path = f"{PREFIX}/{self.attribute}/attributes" 41 | if not self.check_init(): 42 | return 43 | if not os.path.exists(base_path): 44 | logger.error(f"Attribute {self.attribute} not found") 45 | return 46 | try: 47 | self.set_profile() 48 | min_tdp = self._get_min_tdp() 49 | max_tdp = self._get_max_tdp() 50 | if min_tdp is not None and tdp < min_tdp: 51 | logger.info(f"TDP is too low, min: {min_tdp}, use default method") 52 | return self.fallback_set_tdp(tdp) 53 | if max_tdp is not None and tdp > max_tdp: 54 | logger.info(f"TDP is too high, max: {max_tdp}, set to max") 55 | tdp = max_tdp 56 | if os.path.exists(f"{base_path}/{SPL_SUFFIX}/current_value"): 57 | with open(f"{base_path}/{SPL_SUFFIX}/current_value", "w") as f: 58 | f.write(str(tdp)) 59 | sleep(0.1) 60 | if os.path.exists(f"{base_path}/{SLOW_SUFFIX}/current_value"): 61 | with open(f"{base_path}/{SLOW_SUFFIX}/current_value", "w") as f: 62 | f.write(str(tdp)) 63 | sleep(0.1) 64 | if os.path.exists(f"{base_path}/{FAST_SUFFIX}/current_value"): 65 | with open(f"{base_path}/{FAST_SUFFIX}/current_value", "w") as f: 66 | f.write(str(tdp)) 67 | sleep(0.1) 68 | except Exception as e: 69 | logger.error(f"Failed to set TDP: {e}", exc_info=True) 70 | 71 | def set_tdp_unlimited(self) -> None: 72 | logger.info("Setting TDP unlimited") 73 | try: 74 | if self.supports_attribute_tdp(): 75 | fast_max = 0 76 | slow_max = 0 77 | stapm_max = 0 78 | base_path = f"{PREFIX}/{self.attribute}/attributes" 79 | if os.path.exists(f"{base_path}/{SPL_SUFFIX}/max_value"): 80 | with open(f"{base_path}/{SPL_SUFFIX}/max_value", "r") as f: 81 | fast_max = f.read().strip() 82 | if os.path.exists(f"{base_path}/{SLOW_SUFFIX}/max_value"): 83 | with open(f"{base_path}/{SLOW_SUFFIX}/max_value", "r") as f: 84 | slow_max = f.read().strip() 85 | if os.path.exists(f"{base_path}/{FAST_SUFFIX}/max_value"): 86 | with open(f"{base_path}/{FAST_SUFFIX}/max_value", "r") as f: 87 | stapm_max = f.read().strip() 88 | 89 | if int(fast_max) > 0: 90 | logger.info(f"Setting TDP max to {fast_max} for fast") 91 | with open(f"{base_path}/{SPL_SUFFIX}/current_value", "w") as f: 92 | f.write(fast_max) 93 | if int(slow_max) > 0: 94 | logger.info(f"Setting TDP max to {slow_max} for slow") 95 | with open(f"{base_path}/{SLOW_SUFFIX}/current_value", "w") as f: 96 | f.write(slow_max) 97 | if int(stapm_max) > 0: 98 | logger.info(f"Setting TDP max to {stapm_max} for stapm") 99 | with open(f"{base_path}/{FAST_SUFFIX}/current_value", "w") as f: 100 | f.write(stapm_max) 101 | else: 102 | self.fallback_set_tdp_unlimited() 103 | except Exception as e: 104 | logger.error(f"Failed to set TDP unlimited: {e}", exc_info=True) 105 | 106 | def _get_min_tdp(self) -> int: 107 | if not self.check_init(): 108 | return None 109 | min_tdp = None 110 | base_path = f"{PREFIX}/{self.attribute}/attributes" 111 | if os.path.exists(f"{base_path}/{SPL_SUFFIX}/min_value"): 112 | with open(f"{base_path}/{SPL_SUFFIX}/min_value", "r") as f: 113 | min_tdp = int(f.read()) 114 | return min_tdp 115 | 116 | def _get_max_tdp(self) -> int: 117 | if not self.check_init(): 118 | return None 119 | max_tdp = None 120 | base_path = f"{PREFIX}/{self.attribute}/attributes" 121 | if os.path.exists(f"{base_path}/{SPL_SUFFIX}/max_value"): 122 | with open(f"{base_path}/{SPL_SUFFIX}/max_value", "r") as f: 123 | max_tdp = int(f.read()) 124 | return max_tdp 125 | 126 | def fallback_set_tdp(self, tdp: int) -> None: 127 | logger.info("Device does not support attribute TDP, use fallback method") 128 | return super().set_tdp(tdp) 129 | 130 | def fallback_set_tdp_unlimited(self) -> None: 131 | logger.info("Device does not support attribute TDP, use fallback method") 132 | return super().set_tdp_unlimited() 133 | 134 | def set_profile(self) -> None: 135 | if not self.check_init(): 136 | return 137 | current_profile = self.get_platform_profile(self.profile_name) 138 | available_profiles = self.get_available_platform_profiles(self.profile_name) 139 | profile = None 140 | for suggested_profile in SUGGESTED_DEFAULT: 141 | if suggested_profile in available_profiles: 142 | profile = suggested_profile 143 | break 144 | if current_profile != profile and profile is not None: 145 | logger.info(f"Setting platform profile to {profile}") 146 | self.set_platform_profile(self.profile_name, profile) 147 | sleep(0.5) 148 | -------------------------------------------------------------------------------- /py_modules/devices/idevice.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from config import BOARD_NAME, BOARD_VENDOR, PRODUCT_NAME, PRODUCT_VERSION, VENDOR_NAME 4 | 5 | 6 | class IDevice(ABC): 7 | _instance: "IDevice" = None 8 | vendor_name = VENDOR_NAME 9 | product_name = PRODUCT_NAME 10 | product_version = PRODUCT_VERSION 11 | board_name = BOARD_NAME 12 | board_vendor = BOARD_VENDOR 13 | 14 | @classmethod 15 | def get_current(cls) -> "IDevice": 16 | if cls._instance is not None: 17 | return cls._instance 18 | 19 | match BOARD_VENDOR: 20 | case "AYADEVICE" | "AYANEO": 21 | match BOARD_NAME: 22 | case "AYANEO 2" | "AYANEO 2S" | "GEEK" | "GEEK 1S": 23 | from . import AyaneoDevice 24 | 25 | cls._instance = AyaneoDevice() 26 | case "AB05-AMD" | "AS01": 27 | from . import AyaneoAirPlus 28 | 29 | cls._instance = AyaneoAirPlus() 30 | 31 | case "AB05-Intel": 32 | from . import AyaneoAirPlusIntel 33 | 34 | cls._instance = AyaneoAirPlusIntel() 35 | case "AB05-Mendocino": 36 | from . import AyaneoAirPlusMendocino 37 | 38 | cls._instance = AyaneoAirPlusMendocino() 39 | case "AIR" | "AIR Pro": 40 | from . import AyaneoAir 41 | 42 | cls._instance = AyaneoAir() 43 | case "AIR 1S" | "AIR 1S Limited": 44 | from . import AyaneoAir1S 45 | 46 | cls._instance = AyaneoAir1S() 47 | case "FLIP KB": 48 | from . import AyaneoFlipKB 49 | 50 | cls._instance = AyaneoFlipKB() 51 | case "FLIP DS": 52 | from . import AyaneoFlipDS 53 | 54 | cls._instance = AyaneoFlipDS() 55 | case "KUN": 56 | from . import AyaneoKun 57 | 58 | cls._instance = AyaneoKun() 59 | case _: 60 | from . import PowerDevice 61 | 62 | cls._instance = PowerDevice() 63 | case "ASUSTeK COMPUTER INC.": 64 | from . import AsusDevice 65 | 66 | cls._instance = AsusDevice() 67 | case "Micro-Star International Co., Ltd.": 68 | match BOARD_NAME: 69 | # Claw 8 70 | case "MS-1T52": 71 | from . import MsiClaw8 72 | 73 | cls._instance = MsiClaw8() 74 | case "MS-1T41": 75 | from . import MsiClawA1M 76 | 77 | cls._instance = MsiClawA1M() 78 | case _: 79 | from . import MsiDevice 80 | 81 | cls._instance = MsiDevice() 82 | case "LENOVO": 83 | from . import LenovoDevice 84 | 85 | cls._instance = LenovoDevice() 86 | case _: 87 | from . import PowerDevice 88 | 89 | cls._instance = PowerDevice() 90 | 91 | return cls._instance 92 | 93 | @abstractmethod 94 | def get_bypass_charge(self) -> bool | None: 95 | pass 96 | 97 | @abstractmethod 98 | def set_bypass_charge(self, value: bool) -> None: 99 | pass 100 | 101 | @abstractmethod 102 | def set_charge_limit(self, value: int) -> None: 103 | pass 104 | 105 | @abstractmethod 106 | def supports_bypass_charge(self) -> bool: 107 | pass 108 | 109 | @abstractmethod 110 | def supports_charge_limit(self) -> bool: 111 | pass 112 | 113 | @abstractmethod 114 | def reset_charge_limit(self) -> None: 115 | pass 116 | 117 | # software_charge_limit 118 | @abstractmethod 119 | def software_charge_limit(self) -> bool: 120 | pass 121 | 122 | @abstractmethod 123 | def supports_reset_charge_limit(self) -> bool: 124 | pass 125 | 126 | @abstractmethod 127 | def load(self) -> None: 128 | pass 129 | 130 | @abstractmethod 131 | def unload(self) -> None: 132 | pass 133 | 134 | @abstractmethod 135 | def supports_sched_ext(self) -> bool: 136 | pass 137 | 138 | @abstractmethod 139 | def get_sched_ext_list(self) -> list[str]: 140 | pass 141 | 142 | @abstractmethod 143 | def set_sched_ext(self, value: str, param: str) -> None: 144 | pass 145 | 146 | @abstractmethod 147 | def set_tdp(self, tdp: int) -> bool: 148 | pass 149 | 150 | @abstractmethod 151 | def set_tdp_unlimited(self) -> bool: 152 | pass 153 | -------------------------------------------------------------------------------- /py_modules/devices/lenovo/lenovo_device.py: -------------------------------------------------------------------------------- 1 | from ..firmware_attribute_device import FirmwareAttributeDevice 2 | 3 | ATTRIBUTE_NAME = "lenovo-wmi-other-0" 4 | PLATFORM_PROFILE_NAME = "lenovo-wmi-gamezone" 5 | SUGGESTED_DEFAULT = "custom" 6 | 7 | 8 | class LenovoDevice(FirmwareAttributeDevice): 9 | def __init__(self) -> None: 10 | super().__init__() 11 | self.init_attribute(ATTRIBUTE_NAME, PLATFORM_PROFILE_NAME) 12 | -------------------------------------------------------------------------------- /py_modules/devices/msi/msi_claw8.py: -------------------------------------------------------------------------------- 1 | from .msi_device import SM_COMFORT_NAME, SM_ECO_NAME, SM_SPORT_NAME, MsiDevice 2 | 3 | SHIFT_MODES_DICT = { 4 | SM_ECO_NAME: 0xC2, 5 | SM_COMFORT_NAME: 0xC1, 6 | SM_SPORT_NAME: 0xC0, 7 | } 8 | 9 | 10 | class MsiClaw8(MsiDevice): 11 | def __init__(self): 12 | super().__init__() 13 | self.shift_mode_dict = SHIFT_MODES_DICT 14 | -------------------------------------------------------------------------------- /py_modules/devices/msi/msi_claw_a1m.py: -------------------------------------------------------------------------------- 1 | from .msi_device import SM_COMFORT_NAME, SM_ECO_NAME, SM_SPORT_NAME, MsiDevice 2 | 3 | SHIFT_MODES_DICT = { 4 | SM_ECO_NAME: 0xC2, 5 | SM_COMFORT_NAME: 0xC1, 6 | SM_SPORT_NAME: 0xC0, 7 | } 8 | 9 | 10 | class MsiClawA1M(MsiDevice): 11 | def __init__(self): 12 | super().__init__() 13 | self.shift_mode_dict = SHIFT_MODES_DICT 14 | -------------------------------------------------------------------------------- /py_modules/devices/msi/msi_device.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from config import logger 4 | from utils import support_charge_control_end_threshold 5 | 6 | from ..firmware_attribute_device import FirmwareAttributeDevice 7 | 8 | 9 | EC_ADDR_UNSUPP = 0xFF01 10 | 11 | EC_CHARGE_LIMIT_ADDR = 0xD7 12 | EC_CHARGE_LIMIT_DISABLE = 0x80 13 | 14 | EC_SHIFT_MODE_ADDR = 0xD2 15 | 16 | SM_ECO_NAME = "eco" 17 | SM_COMFORT_NAME = "comfort" 18 | SM_SPORT_NAME = "sport" 19 | SM_TURBO_NAME = "turbo" 20 | 21 | SHIFT_MODES_DICT = { 22 | SM_ECO_NAME: 0xC2, 23 | SM_COMFORT_NAME: 0xC1, 24 | SM_SPORT_NAME: 0xC0, 25 | SM_TURBO_NAME: 0xC4, 26 | } 27 | 28 | SHIFT_MODE_PATH = "/sys/devices/platform/msi-ec/shift_mode" 29 | AVAILBLE_SHIFT_MODES_PATH = "/sys/devices/platform/msi-ec/available_shift_modes" 30 | 31 | ATTRIBUTE_NAME = "msi-wmi-platform" 32 | PLATFORM_PROFILE_NAME = "msi-wmi-platform" 33 | SUGGESTED_DEFAULT = ["custom", "performance"] 34 | 35 | 36 | class MsiDevice(FirmwareAttributeDevice): 37 | def __init__(self): 38 | super().__init__() 39 | self.ec_addr_unsupp = EC_ADDR_UNSUPP 40 | self.ec_shift_mode_addr = EC_SHIFT_MODE_ADDR 41 | self.shift_mode_dict = SHIFT_MODES_DICT 42 | self.shift_mode_path = SHIFT_MODE_PATH 43 | self.availble_shift_modes_path = AVAILBLE_SHIFT_MODES_PATH 44 | self.init_attribute(ATTRIBUTE_NAME, PLATFORM_PROFILE_NAME) 45 | 46 | def _shift_mode_sysfs(self, mode_name: str) -> None: 47 | # read available shift modes 48 | with open(self.availble_shift_modes_path, "r") as f: 49 | available_shift_modes = f.read().strip().split("\n") 50 | 51 | # check if mode is available 52 | if mode_name not in available_shift_modes: 53 | logger.error(f"Mode {mode_name} is not available") 54 | return 55 | # current mode 56 | with open(self.shift_mode_path, "r") as f: 57 | current_mode = f.read().strip() 58 | 59 | # write mode 60 | if current_mode == mode_name: 61 | return 62 | 63 | logger.info(f"Writing shift mode sysfs {mode_name}") 64 | with open(self.shift_mode_path, "w") as f: 65 | f.write(mode_name) 66 | 67 | def _shift_mode_ec(self, mode_name: str) -> None: 68 | # check if mode is available 69 | if mode_name not in self.shift_mode_dict: 70 | logger.error(f"Mode {mode_name} is not available") 71 | return 72 | 73 | # current mode 74 | current_mode = self._ec_read(self.ec_shift_mode_addr) 75 | if current_mode == self.shift_mode_dict[mode_name]: 76 | return 77 | 78 | # write mode 79 | logger.info(f"Writing shift mode EC {mode_name}") 80 | self._ec_write(self.ec_shift_mode_addr, self.shift_mode_dict[mode_name]) 81 | 82 | def shift_mode_write(self, mode_name: str) -> None: 83 | if self.support_shift_mode(): 84 | self._shift_mode_sysfs(mode_name) 85 | else: 86 | self._shift_mode_ec(mode_name) 87 | 88 | def support_shift_mode(self) -> bool: 89 | return os.path.exists(self.shift_mode_path) 90 | 91 | def supports_reset_charge_limit(self) -> bool: 92 | return True 93 | 94 | def supports_charge_limit(self) -> bool: 95 | return True 96 | 97 | def reset_charge_limit(self) -> None: 98 | self._ec_write(EC_CHARGE_LIMIT_ADDR, EC_CHARGE_LIMIT_DISABLE) 99 | 100 | def set_charge_limit(self, value: int) -> None: 101 | if support_charge_control_end_threshold(): 102 | super().set_charge_limit(value) 103 | else: 104 | if not 0 <= value <= 100: 105 | logger.error( 106 | f"Charge limit must be between 0-100, current value: {value}" 107 | ) 108 | return 109 | write_value = value + EC_CHARGE_LIMIT_DISABLE 110 | self._ec_write(EC_CHARGE_LIMIT_ADDR, write_value) 111 | 112 | def run_before_set_tdp(self): 113 | try: 114 | self.shift_mode_write(SM_SPORT_NAME) 115 | except Exception as e: 116 | logger.error(f"Failed to run before set tdp: {e}") 117 | -------------------------------------------------------------------------------- /py_modules/ec.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import portio 4 | from config import logger 5 | 6 | EC_CMD_STATUS_REGISTER_PORT = 0x66 7 | EC_DATA_REGISTER_PORT = 0x62 8 | EC_IBF_BIT = 1 9 | EC_OBF_BIT = 0 10 | RD_EC = 0x80 # Read Embedded Controller 11 | WR_EC = 0x81 # Write Embedded Controller 12 | 13 | # for register in [EC_DATA_REGISTER_PORT, EC_CMD_STATUS_REGISTER_PORT]: 14 | # status = portio.ioperm(register, 1, 1) 15 | status = portio.iopl(3) 16 | 17 | 18 | class EC: 19 | @staticmethod 20 | def Wait(port, flag, value): 21 | for i in range(200): 22 | data = portio.inb(port) 23 | if ((data >> flag) & 0x1) == value: 24 | condition = True 25 | break 26 | time.sleep(0.001) 27 | 28 | @staticmethod 29 | def Read(address: int): 30 | EC.Wait(EC_CMD_STATUS_REGISTER_PORT, EC_IBF_BIT, 0) 31 | portio.outb(RD_EC, EC_CMD_STATUS_REGISTER_PORT) 32 | EC.Wait(EC_CMD_STATUS_REGISTER_PORT, EC_IBF_BIT, 0) 33 | portio.outb(address, EC_DATA_REGISTER_PORT) 34 | EC.Wait(EC_CMD_STATUS_REGISTER_PORT, EC_OBF_BIT, 1) 35 | result = portio.inb(EC_DATA_REGISTER_PORT) 36 | logger.debug(f"ECRead address:{hex(address)} value:{result}") 37 | return result 38 | 39 | @staticmethod 40 | def ReadLonger(address: int, length: int): 41 | sum = 0 42 | for len in range(length): 43 | value = EC.Read(address + len) 44 | sum = (sum << 8) + value 45 | # logger.debug(f"count={len} sum={sum} address={address+len} value={value}") 46 | logger.debug(f"ECReadLonger address:{hex(address)} value:{sum}") 47 | return sum 48 | 49 | @staticmethod 50 | def Write(address: int, data: int): 51 | EC.Wait(EC_CMD_STATUS_REGISTER_PORT, EC_IBF_BIT, 0) 52 | portio.outb(WR_EC, EC_CMD_STATUS_REGISTER_PORT) 53 | EC.Wait(EC_CMD_STATUS_REGISTER_PORT, EC_IBF_BIT, 0) 54 | portio.outb(address, EC_DATA_REGISTER_PORT) 55 | EC.Wait(EC_CMD_STATUS_REGISTER_PORT, EC_IBF_BIT, 0) 56 | portio.outb(data, EC_DATA_REGISTER_PORT) 57 | EC.Wait(EC_CMD_STATUS_REGISTER_PORT, EC_IBF_BIT, 0) 58 | logger.debug(f"ECWrite address:{hex(address)} value:{data}") 59 | 60 | @staticmethod 61 | def RamWrite(comm_port: int, data_port: int, address: int, data: int): 62 | high_byte = (address >> 8) & 0xFF 63 | low_byte = address & 0xFF 64 | portio.outb(0x2E, comm_port) 65 | portio.outb(0x11, data_port) 66 | portio.outb(0x2F, comm_port) 67 | portio.outb(high_byte, data_port) 68 | 69 | portio.outb(0x2E, comm_port) 70 | portio.outb(0x10, data_port) 71 | portio.outb(0x2F, comm_port) 72 | portio.outb(low_byte, data_port) 73 | 74 | portio.outb(0x2E, comm_port) 75 | portio.outb(0x12, data_port) 76 | portio.outb(0x2F, comm_port) 77 | portio.outb(data, data_port) 78 | logger.debug( 79 | f"ECRamWrite high_byte={hex(high_byte)} low_byte={hex(low_byte)} address:{hex(address)} value:{data}" 80 | ) 81 | 82 | @staticmethod 83 | def RamRead(comm_port: int, data_port: int, address: int): 84 | high_byte = (address >> 8) & 0xFF 85 | low_byte = address & 0xFF 86 | portio.outb(0x2E, comm_port) 87 | portio.outb(0x11, data_port) 88 | portio.outb(0x2F, comm_port) 89 | portio.outb(high_byte, data_port) 90 | 91 | portio.outb(0x2E, comm_port) 92 | portio.outb(0x10, data_port) 93 | portio.outb(0x2F, comm_port) 94 | portio.outb(low_byte, data_port) 95 | 96 | portio.outb(0x2E, comm_port) 97 | portio.outb(0x12, data_port) 98 | portio.outb(0x2F, comm_port) 99 | data = portio.inb(data_port) 100 | logger.debug( 101 | f"ECRamRead high_byte={hex(high_byte)} low_byte={hex(low_byte)} address:{hex(address)} value:{data}" 102 | ) 103 | return data 104 | 105 | @staticmethod 106 | def RamReadLonger(reg_addr: int, reg_data: int, address: int, length: int): 107 | sum = 0 108 | for len in range(length): 109 | value = EC.RamRead(reg_addr, reg_data, address + len) 110 | sum = (sum << 8) + value 111 | # logger.debug(f"count={len} sum={sum} address={address+len} value={value}") 112 | logger.debug(f"ECReadLonger address:{hex(address)} value:{sum}") 113 | return sum 114 | 115 | def PrintAll(): 116 | print("", "\t", end="") 117 | for z in range(0xF + 1): 118 | print(hex(z), "\t", end="") 119 | print() 120 | for x in range(0xF + 1): 121 | for y in range(0xF + 1): 122 | if y == 0x00: 123 | print(hex(x), "\t", end="") 124 | print(EC.Read((x << 4) + y), "\t", end="") 125 | print() 126 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/aokzoe_a.yml: -------------------------------------------------------------------------------- 1 | product_name: 2 | - AOKZOE A1 Pro 3 | - AOKZOE A2 Pro 4 | fans: 5 | - manual_offset: 0x4a # 风扇自动控制ec地址 6 | rpmwrite_offset: 0x4b # 风扇写入转速ec地址 7 | rpmread_offset: 0x76 # 风扇读取转速ec地址 8 | 9 | ram_reg_addr: 0x4E 10 | ram_reg_data: 0x4F 11 | ram_manual_offset: 0x44a 12 | ram_rpmwrite_offset: 0x44b 13 | ram_rpmread_offset: 0x1809 14 | ram_rpmread_length: 0 15 | rpm_write_max: 184 16 | rpm_value_max: 5000 17 | 18 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/ayaneo_2.yml: -------------------------------------------------------------------------------- 1 | product_name: 2 | - AYANEO 2 3 | - AYANEO 2S 4 | - GEEK 5 | - GEEK 1S 6 | - FLIP KD 7 | - FLIP DS 8 | fans: 9 | - manual_offset: 0x4a 10 | rpmwrite_offset: 0x4b 11 | rpmread_offset: 0x76 12 | 13 | ram_reg_addr: 0x4E 14 | ram_reg_data: 0x4F 15 | ram_manual_offset: 0x44a 16 | ram_rpmwrite_offset: 0x44b 17 | ram_rpmread_offset: 0x1809 18 | ram_rpmread_length: 0 19 | 20 | rpm_write_max: 255 21 | rpm_value_max: 5530 22 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/ayaneo_air.yml: -------------------------------------------------------------------------------- 1 | product_name: 2 | - AIR 3 | - AIR Pro 4 | - AIR 1S 5 | - AIR 1S Limited 6 | fans: 7 | - manual_offset: 0x4a 8 | rpmwrite_offset: 0x4b 9 | rpmread_offset: 0x76 10 | 11 | ram_reg_addr: 0x4E 12 | ram_reg_data: 0x4F 13 | ram_manual_offset: 0x44a 14 | ram_rpmwrite_offset: 0x44b 15 | ram_rpmread_offset: 0x1809 16 | ram_rpmread_length: 0 17 | 18 | rpm_write_max: 255 19 | rpm_value_max: 5811 20 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/ayaneo_airplus.yml: -------------------------------------------------------------------------------- 1 | # 转速的读取地址不对 2 | # 写入转速测试可用 3 | product_name: 4 | - AIR Plus 5 | - SLIDE 6 | fans: 7 | - ram_reg_addr: 0x4E 8 | ram_reg_data: 0x4F 9 | ram_manual_offset: 0xd1c8 10 | ram_rpmwrite_offset: 0x1804 11 | ram_rpmread_offset: 0x1804 12 | ram_rpmread_length: 0 13 | enable_manual_value: 0xa5 14 | enable_auto_value: 0x00 15 | 16 | # manual_offset: 0x4a 17 | # rpmwrite_offset: 0x4b 18 | # rpmread_offset: 0x76 19 | 20 | rpm_write_max: 255 21 | rpm_value_max: 6000 22 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/gpd_win4.yml: -------------------------------------------------------------------------------- 1 | product_name: 2 | - G1618-04 3 | fans: 4 | - ram_reg_addr: 0x2E 5 | ram_reg_data: 0x2F 6 | ram_manual_offset: 0xC311 7 | ram_rpmwrite_offset: 0xC311 8 | ram_rpmread_offset: 0x880 9 | ram_rpmread_length: 2 10 | 11 | rpm_write_max: 127 12 | rpm_value_max: 4968 13 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/gpd_win4_ver1.yml: -------------------------------------------------------------------------------- 1 | product_name: 2 | - G1618-04 3 | product_version: 4 | - Ver. 1.0 5 | - Ver.1.0 6 | fans: 7 | - ram_reg_addr: 0x4E 8 | ram_reg_data: 0x4F 9 | ram_manual_offset: 0x275 10 | ram_rpmwrite_offset: 0x1809 11 | ram_rpmread_offset: 0x218 12 | ram_rpmread_length: 2 13 | 14 | rpm_write_max: 184 15 | rpm_value_max: 4968 16 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/gpd_winmax2.yml: -------------------------------------------------------------------------------- 1 | product_name: 2 | - G1619-04 3 | fans: 4 | - ram_reg_addr: 0x4E 5 | ram_reg_data: 0x4F 6 | ram_manual_offset: 0x275 7 | ram_rpmwrite_offset: 0x1809 8 | ram_rpmread_offset: 0x218 9 | ram_rpmread_length: 2 10 | 11 | rpm_write_max: 184 12 | rpm_value_max: 4968 13 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/gpd_winmini.yml: -------------------------------------------------------------------------------- 1 | # ECIO 或者 ECRAM 都可以 2 | # 内核补丁使用的是 ECIO 的方式 3 | product_name: 4 | - G1617-01 5 | - G1617-02 6 | fans: 7 | - ram_reg_addr: 0x4E 8 | ram_reg_data: 0x4F 9 | ram_manual_offset: 0x47A 10 | ram_rpmwrite_offset: 0x47A 11 | ram_rpmread_offset: 0x478 12 | ram_rpmread_length: 2 13 | 14 | manual_offset: 0x7a 15 | rpmwrite_offset: 0x7a 16 | rpmread_offset: 0x78 17 | 18 | rpm_write_max: 244 19 | rpm_value_max: 6700 20 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/onexplayer2.yml: -------------------------------------------------------------------------------- 1 | product_name: 2 | - ONEXPLAYER 2 ARP23 3 | - ONEXPLAYER 2 PRO ARP23P 4 | - ONEXPLAYER 2 PRO ARP23P EVA-01 5 | - ONEXPLAYER X1 6 | - ONEXPLAYER X1 A 7 | - ONEXPLAYER X1 mini 8 | fans: 9 | - manual_offset: 0x4a 10 | rpmwrite_offset: 0x4b 11 | rpmread_offset: 0x58 12 | 13 | ram_reg_addr: 0x4E 14 | ram_reg_data: 0x4F 15 | ram_manual_offset: 0x44a 16 | ram_rpmwrite_offset: 0x44b 17 | ram_rpmread_offset: 0x1809 18 | ram_rpmread_length: 0 19 | 20 | rpm_write_max: 184 21 | rpm_value_max: 5000 22 | -------------------------------------------------------------------------------- /py_modules/fan_config/ec/onexplayer_mini.yml: -------------------------------------------------------------------------------- 1 | product_name: # /sys/devices/virtual/dmi/id/product_name 内容 2 | - AOKZOE A1 AR07 3 | - ONEXPLAYER Mini Pro 4 | - ONEXPLAYER mini A07 5 | fans: 6 | - manual_offset: 0x4a # 风扇自动控制ec地址 7 | rpmwrite_offset: 0x4b # 风扇写入转速ec地址 8 | rpmread_offset: 0x76 # 风扇读取转速ec地址 9 | 10 | ram_reg_addr: 0x4E # 风扇ecRam寄存器地址 11 | ram_reg_data: 0x4F # 风扇ecRam寄存器数据 12 | ram_manual_offset: 0x44a # 风扇自动控制ecRam地址 13 | ram_rpmwrite_offset: 0x44b # 风扇写入转速ecRam地址 14 | ram_rpmread_offset: 0x1809 # 风扇读取转速ecRam地址 15 | ram_rpmread_length: 0 # 风扇实际转速值长度 0为需要通过计算获得转速 16 | 17 | rpm_write_max: 255 # 风扇最大转速ec写入值 18 | rpm_value_max: 4968 # 风扇最大转速数值 19 | 20 | -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/asus.yml: -------------------------------------------------------------------------------- 1 | # 略微修改的曲线 提高了高温点的转速 2 | hwmon_name: asus_custom_fan_curve 3 | 4 | fans: 5 | - fan_name: Fan1 6 | pwm_mode: 2 7 | 8 | pwm_enable: 9 | manual_value: 1 10 | auto_value: 2 # 设置自动模式会使用内置的曲线,会受到platform_profile影响,如果要在设置为自动的时候使用下面的曲线,需要设置为1 11 | pwm_enable_path: pwm1_enable 12 | 13 | pwm_write: 14 | pwm_write_max: 15 | default: 255 16 | ROG Ally RC71L_RC71L: 255 17 | curve_path: # 曲线写入路径 18 | temp_write: 19 | - pwm1_auto_point1_temp 20 | - pwm1_auto_point2_temp 21 | - pwm1_auto_point3_temp 22 | - pwm1_auto_point4_temp 23 | - pwm1_auto_point5_temp 24 | - pwm1_auto_point6_temp 25 | - pwm1_auto_point7_temp 26 | - pwm1_auto_point8_temp 27 | pwm_write: 28 | - pwm1_auto_point1_pwm 29 | - pwm1_auto_point2_pwm 30 | - pwm1_auto_point3_pwm 31 | - pwm1_auto_point4_pwm 32 | - pwm1_auto_point5_pwm 33 | - pwm1_auto_point6_pwm 34 | - pwm1_auto_point7_pwm 35 | - pwm1_auto_point8_pwm 36 | default_curve: 37 | temp: 38 | - 34 39 | - 40 40 | - 50 41 | - 60 42 | - 70 43 | - 80 44 | - 90 45 | - 100 46 | speed: 47 | - 1 48 | - 4 49 | - 8 50 | - 48 51 | - 74 52 | - 90 53 | - 95 54 | - 100 55 | pwm: 56 | - 3 57 | - 10 58 | - 20 59 | - 122 60 | - 189 61 | - 255 62 | - 255 63 | - 255 64 | 65 | pwm_input: 66 | hwmon_label: asus 67 | pwm_read_path: fan1_input 68 | pwm_read_max: 8200 69 | 70 | temp_mode: 0 71 | 72 | - fan_name: Fan2 73 | pwm_mode: 2 74 | 75 | pwm_enable: 76 | manual_value: 1 77 | auto_value: 2 78 | pwm_enable_path: pwm2_enable 79 | 80 | pwm_write: 81 | pwm_write_max: 82 | default: 255 83 | ROG Ally RC71L_RC71L: 255 84 | curve_path: # 曲线写入路径 85 | temp_write: 86 | - pwm2_auto_point1_temp 87 | - pwm2_auto_point2_temp 88 | - pwm2_auto_point3_temp 89 | - pwm2_auto_point4_temp 90 | - pwm2_auto_point5_temp 91 | - pwm2_auto_point6_temp 92 | - pwm2_auto_point7_temp 93 | - pwm2_auto_point8_temp 94 | pwm_write: 95 | - pwm2_auto_point1_pwm 96 | - pwm2_auto_point2_pwm 97 | - pwm2_auto_point3_pwm 98 | - pwm2_auto_point4_pwm 99 | - pwm2_auto_point5_pwm 100 | - pwm2_auto_point6_pwm 101 | - pwm2_auto_point7_pwm 102 | - pwm2_auto_point8_pwm 103 | default_curve: 104 | temp: 105 | - 39 106 | - 40 107 | - 50 108 | - 60 109 | - 70 110 | - 80 111 | - 90 112 | - 100 113 | speed: 114 | - 1 115 | - 1 116 | - 1 117 | - 45 118 | - 55 119 | - 65 120 | - 74 121 | - 90 122 | pwm: 123 | - 3 124 | - 3 125 | - 3 126 | - 115 127 | - 140 128 | - 165 129 | - 190 130 | - 229 131 | 132 | pwm_input: 133 | hwmon_label: asus 134 | pwm_read_path: fan2_input 135 | pwm_read_max: 8200 136 | 137 | temp_mode: 0 138 | -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/asus_orig.yml: -------------------------------------------------------------------------------- 1 | # 原生曲线? 暂时不用 修改 hwmon_name 避免匹配 2 | hwmon_name: asus_custom_fan_curve_orig 3 | 4 | fans: 5 | - fan_name: Fan1 6 | pwm_mode: 2 7 | 8 | pwm_enable: 9 | manual_value: 1 10 | auto_value: 2 11 | pwm_enable_path: pwm1_enable 12 | 13 | pwm_write: 14 | pwm_write_max: 15 | default: 255 16 | ROG Ally RC71L_RC71L: 255 17 | curve_path: # 曲线写入路径 18 | temp_write: 19 | - pwm1_auto_point1_temp 20 | - pwm1_auto_point2_temp 21 | - pwm1_auto_point3_temp 22 | - pwm1_auto_point4_temp 23 | - pwm1_auto_point5_temp 24 | - pwm1_auto_point6_temp 25 | - pwm1_auto_point7_temp 26 | - pwm1_auto_point8_temp 27 | pwm_write: 28 | - pwm1_auto_point1_pwm 29 | - pwm1_auto_point2_pwm 30 | - pwm1_auto_point3_pwm 31 | - pwm1_auto_point4_pwm 32 | - pwm1_auto_point5_pwm 33 | - pwm1_auto_point6_pwm 34 | - pwm1_auto_point7_pwm 35 | - pwm1_auto_point8_pwm 36 | default_curve: 37 | temp: 38 | - 34 39 | - 40 40 | - 50 41 | - 60 42 | - 70 43 | - 80 44 | - 90 45 | - 100 46 | speed: 47 | - 1 48 | - 4 49 | - 8 50 | - 48 51 | - 74 52 | - 74 53 | - 74 54 | - 74 55 | pwm: 56 | - 3 57 | - 10 58 | - 20 59 | - 122 60 | - 189 61 | - 189 62 | - 189 63 | - 189 64 | 65 | pwm_input: 66 | hwmon_label: asus 67 | pwm_read_path: fan1_input 68 | pwm_read_max: 8200 69 | 70 | temp_mode: 0 71 | 72 | - fan_name: Fan2 73 | pwm_mode: 2 74 | 75 | pwm_enable: 76 | manual_value: 1 77 | auto_value: 2 78 | pwm_enable_path: pwm2_enable 79 | 80 | pwm_write: 81 | pwm_write_max: 82 | default: 255 83 | ROG Ally RC71L_RC71L: 255 84 | curve_path: # 曲线写入路径 85 | temp_write: 86 | - pwm2_auto_point1_temp 87 | - pwm2_auto_point2_temp 88 | - pwm2_auto_point3_temp 89 | - pwm2_auto_point4_temp 90 | - pwm2_auto_point5_temp 91 | - pwm2_auto_point6_temp 92 | - pwm2_auto_point7_temp 93 | - pwm2_auto_point8_temp 94 | pwm_write: 95 | - pwm2_auto_point1_pwm 96 | - pwm2_auto_point2_pwm 97 | - pwm2_auto_point3_pwm 98 | - pwm2_auto_point4_pwm 99 | - pwm2_auto_point5_pwm 100 | - pwm2_auto_point6_pwm 101 | - pwm2_auto_point7_pwm 102 | - pwm2_auto_point8_pwm 103 | default_curve: 104 | temp: 105 | - 39 106 | - 40 107 | - 50 108 | - 60 109 | - 70 110 | - 80 111 | - 90 112 | - 100 113 | speed: 114 | - 1 115 | - 1 116 | - 1 117 | - 45 118 | - 45 119 | - 45 120 | - 45 121 | - 45 122 | pwm: 123 | - 3 124 | - 3 125 | - 3 126 | - 115 127 | - 115 128 | - 115 129 | - 115 130 | - 115 131 | 132 | pwm_input: 133 | hwmon_label: asus 134 | pwm_read_path: fan2_input 135 | pwm_read_max: 8200 136 | 137 | temp_mode: 0 138 | -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/aynec.yml: -------------------------------------------------------------------------------- 1 | hwmon_name: aynec 2 | 3 | fans: 4 | - fan_name: Fan 5 | pwm_mode: 0 6 | 7 | pwm_enable: 8 | manual_value: 1 9 | auto_value: 0 10 | pwm_enable_path: pwm1_enable 11 | pwm_enable_second_path: pwm1_mode 12 | 13 | pwm_write: 14 | pwm_write_max: 15 | default: 255 16 | pwm_write_path: pwm1 17 | 18 | pwm_input: 19 | hwmon_label: aynec 20 | pwm_read_path: fan1_input 21 | pwm_read_max: 5000 22 | 23 | temp_mode: 0 24 | -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/gpdfan.yml: -------------------------------------------------------------------------------- 1 | # support for https://github.com/Cryolitia/gpd-fan-driver 2 | hwmon_name: gpdfan 3 | 4 | fans: 5 | - fan_name: Fan 6 | pwm_mode: 0 7 | black_list: 8 | - G1618-04 9 | # - G1617-01 10 | 11 | pwm_enable: 12 | manual_value: 1 13 | auto_value: 2 14 | pwm_enable_path: pwm1_enable 15 | 16 | pwm_write: 17 | pwm_write_max: 18 | default: 255 19 | pwm_write_path: pwm1 20 | 21 | pwm_input: 22 | hwmon_label: gpdfan 23 | pwm_read_path: fan1_input 24 | pwm_read_max: 5000 25 | 26 | temp_mode: 0 -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/msi_ec.yml: -------------------------------------------------------------------------------- 1 | hwmon_name: msi_ec 2 | 3 | fans: 4 | - fan_name: Fan1 5 | pwm_mode: 2 # 写入的模式 0.普通模式(单文件写入) 1.对多个文件写入同样的数值 2.对多个文件写入不同的数值 6 | 7 | pwm_enable: 8 | manual_value: 1 9 | auto_value: 2 10 | pwm_enable_path: pwm1_enable 11 | 12 | pwm_write: 13 | pwm_write_max: 14 | default: 170 # 100% 15 | # Claw 8 AI+ A2VM: 255 # 150% 16 | curve_path: # 曲线写入路径 17 | temp_write: 18 | - pwm1_auto_point1_temp 19 | - pwm1_auto_point2_temp 20 | - pwm1_auto_point3_temp 21 | - pwm1_auto_point4_temp 22 | - pwm1_auto_point5_temp 23 | - pwm1_auto_point6_temp 24 | pwm_write: 25 | - pwm1_auto_point1_pwm 26 | - pwm1_auto_point2_pwm 27 | - pwm1_auto_point3_pwm 28 | - pwm1_auto_point4_pwm 29 | - pwm1_auto_point5_pwm 30 | - pwm1_auto_point6_pwm 31 | default_curve: 32 | temp: 33 | - 50 34 | - 60 35 | - 70 36 | - 80 37 | - 88 38 | - 88 39 | pwm: 40 | - 0 41 | - 68 42 | - 83 43 | - 98 44 | - 113 45 | - 127 46 | 47 | pwm_input: 48 | hwmon_label: msi_ec 49 | pwm_read_path: fan1_input 50 | pwm_read_max: 5800 51 | 52 | temp_mode: 0 53 | 54 | - fan_name: Fan2 55 | pwm_mode: 2 56 | 57 | pwm_enable: 58 | manual_value: 1 59 | auto_value: 2 60 | pwm_enable_path: pwm2_enable 61 | 62 | pwm_write: 63 | pwm_write_max: 64 | default: 170 # 100% 65 | # Claw 8 AI+ A2VM: 255 # 150% 66 | curve_path: # 曲线写入路径 67 | temp_write: 68 | - pwm2_auto_point1_temp 69 | - pwm2_auto_point2_temp 70 | - pwm2_auto_point3_temp 71 | - pwm2_auto_point4_temp 72 | - pwm2_auto_point5_temp 73 | - pwm2_auto_point6_temp 74 | pwm_write: 75 | - pwm2_auto_point1_pwm 76 | - pwm2_auto_point2_pwm 77 | - pwm2_auto_point3_pwm 78 | - pwm2_auto_point4_pwm 79 | - pwm2_auto_point5_pwm 80 | - pwm2_auto_point6_pwm 81 | default_curve: 82 | temp: 83 | - 50 84 | - 60 85 | - 70 86 | - 80 87 | - 88 88 | - 88 89 | pwm: 90 | - 0 91 | - 68 92 | - 83 93 | - 98 94 | - 113 95 | - 127 96 | 97 | pwm_input: 98 | hwmon_label: msi_ec 99 | pwm_read_path: fan2_input 100 | pwm_read_max: 5800 101 | 102 | temp_mode: 0 103 | -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/msi_wmi.yml: -------------------------------------------------------------------------------- 1 | hwmon_name: msi_wmi_platform 2 | 3 | fans: 4 | - fan_name: Fan1 5 | pwm_mode: 2 # 写入的模式 0.普通模式(单文件写入) 1.对多个文件写入同样的数值 2.对多个文件写入不同的数值 6 | 7 | pwm_enable: 8 | manual_value: 1 9 | auto_value: 2 10 | pwm_enable_path: pwm1_enable 11 | 12 | pwm_write: 13 | pwm_write_max: 14 | default: 255 # 100% 15 | curve_path: # 曲线写入路径 16 | temp_write: 17 | - pwm1_auto_point1_temp 18 | - pwm1_auto_point2_temp 19 | - pwm1_auto_point3_temp 20 | - pwm1_auto_point4_temp 21 | - pwm1_auto_point5_temp 22 | - pwm1_auto_point6_temp 23 | pwm_write: 24 | - pwm1_auto_point1_pwm 25 | - pwm1_auto_point2_pwm 26 | - pwm1_auto_point3_pwm 27 | - pwm1_auto_point4_pwm 28 | - pwm1_auto_point5_pwm 29 | - pwm1_auto_point6_pwm 30 | default_curve: 31 | temp: 32 | - 0 33 | - 50 34 | - 60 35 | - 70 36 | - 80 37 | - 90 38 | pwm: 39 | - 0 40 | - 102 41 | - 124 42 | - 147 43 | - 170 44 | - 191 45 | 46 | pwm_input: 47 | hwmon_label: msi_wmi_platform 48 | pwm_read_path: fan1_input 49 | pwm_read_max: 5800 50 | 51 | temp_mode: 0 52 | 53 | - fan_name: Fan2 54 | pwm_mode: 2 55 | 56 | pwm_enable: 57 | manual_value: 1 58 | auto_value: 2 59 | pwm_enable_path: pwm2_enable 60 | 61 | pwm_write: 62 | pwm_write_max: 63 | default: 170 # 100% 64 | # Claw 8 AI+ A2VM: 255 # 150% 65 | curve_path: # 曲线写入路径 66 | temp_write: 67 | - pwm2_auto_point1_temp 68 | - pwm2_auto_point2_temp 69 | - pwm2_auto_point3_temp 70 | - pwm2_auto_point4_temp 71 | - pwm2_auto_point5_temp 72 | - pwm2_auto_point6_temp 73 | pwm_write: 74 | - pwm2_auto_point1_pwm 75 | - pwm2_auto_point2_pwm 76 | - pwm2_auto_point3_pwm 77 | - pwm2_auto_point4_pwm 78 | - pwm2_auto_point5_pwm 79 | - pwm2_auto_point6_pwm 80 | default_curve: 81 | temp: 82 | - 0 83 | - 50 84 | - 60 85 | - 70 86 | - 80 87 | - 90 88 | pwm: 89 | - 0 90 | - 102 91 | - 124 92 | - 147 93 | - 170 94 | - 191 95 | 96 | pwm_input: 97 | hwmon_label: msi_wmi_platform 98 | pwm_read_path: fan2_input 99 | pwm_read_max: 5800 100 | 101 | temp_mode: 0 102 | -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/oxp_ec.yml: -------------------------------------------------------------------------------- 1 | # hwmon目录下name文件内容 2 | hwmon_name: oxp_ec 3 | 4 | fans: 5 | - fan_name: Fan # 显示在 UI 上的风扇名称 6 | pwm_mode: 0 # 写入的模式 0.普通模式(对单个文件写入) 1.rog掌机特殊模式(对多个文件写入同样的数值) 7 | black_list: # 黑名单, 匹配 /sys/devices/virtual/dmi/id/product_name. 不使用该配置 8 | - G1618-04 9 | # - G1617-01 10 | 11 | pwm_enable: 12 | manual_value: 1 # 手动模式写入的值 13 | auto_value: 2 # 自动模式写入的值 14 | pwm_enable_path: pwm1_enable # 写入数值的文件路径(数值模式) 15 | 16 | pwm_write: 17 | pwm_write_max: # 写入转速最大值(根据不同产品名写入不同的最大值,没有则使用默认) 18 | default: 255 19 | pwm_write_path: pwm1 # 写入数值的文件路径 20 | 21 | pwm_input: 22 | pwm_read_path: fan1_input # 读取转速的文件路径 23 | pwm_read_max: 5000 # 读取转速的最大值 24 | 25 | temp_mode: 0 #温度使用哪个数据 0.cpu温度(不可用时切换到gpu温度) 1.gpu温度(不可用时切换到cpu温度) 26 | -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/oxpec.yml: -------------------------------------------------------------------------------- 1 | # hwmon目录下name文件内容 2 | hwmon_name: oxpec 3 | 4 | fans: 5 | - fan_name: Fan # 显示在 UI 上的风扇名称 6 | pwm_mode: 0 # 写入的模式 0.普通模式(对单个文件写入) 1.rog掌机特殊模式(对多个文件写入同样的数值) 7 | black_list: # 黑名单, 匹配 /sys/devices/virtual/dmi/id/product_name. 不使用该配置 8 | - G1618-04 9 | # - G1617-01 10 | 11 | pwm_enable: 12 | manual_value: 1 # 手动模式写入的值 13 | auto_value: 0 # 自动模式写入的值 14 | pwm_enable_path: pwm1_enable # 写入数值的文件路径(数值模式) 15 | 16 | pwm_write: 17 | pwm_write_max: # 写入转速最大值(根据不同产品名写入不同的最大值,没有则使用默认) 18 | default: 255 19 | pwm_write_path: pwm1 # 写入数值的文件路径 20 | 21 | pwm_input: 22 | pwm_read_path: fan1_input # 读取转速的文件路径 23 | pwm_read_max: 5000 # 读取转速的最大值 24 | 25 | temp_mode: 0 #温度使用哪个数据 0.cpu温度(不可用时切换到gpu温度) 1.gpu温度(不可用时切换到cpu温度) 26 | -------------------------------------------------------------------------------- /py_modules/fan_config/hwmon/steamdeck.yml: -------------------------------------------------------------------------------- 1 | hwmon_name: steamdeck_hwmon 2 | 3 | fans: 4 | - fan_name: Fan 5 | pwm_mode: 0 6 | 7 | pwm_enable: 8 | manual_value: 1 9 | auto_value: 0 10 | pwm_enable_path: fan1_target 11 | 12 | pwm_write: 13 | pwm_write_max: 14 | default: 7300 15 | pwm_write_path: fan1_target 16 | 17 | pwm_input: 18 | hwmon_label: steamdeck_hwmon 19 | pwm_read_path: fan1_input 20 | pwm_read_max: 7309 21 | 22 | temp_mode: 0 23 | -------------------------------------------------------------------------------- /py_modules/fan_config/schema/ec.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$ref": "#/definitions/ECProfile", 4 | "definitions": { 5 | "ECProfile": { 6 | "title": "ECProfile", 7 | "type": "object", 8 | "additionalProperties": false, 9 | "properties": { 10 | "product_name": { 11 | "type": "array", 12 | "items": { 13 | "type": "string" 14 | }, 15 | "minItems": 1, 16 | "description": "设备标识, `/sys/class/dmi/id/product_name`" 17 | }, 18 | "product_version": { 19 | "type": "array", 20 | "items": { 21 | "type": "string" 22 | }, 23 | "minItems": 1, 24 | "description": "设备版本, `/sys/class/dmi/id/product_version`" 25 | }, 26 | "fans": { 27 | "type": "array", 28 | "items": { 29 | "$ref": "#/definitions/Fans" 30 | }, 31 | "minItems": 1, 32 | "description": "风扇配置信息" 33 | } 34 | }, 35 | "required": ["product_name", "fans"] 36 | }, 37 | "Fans": { 38 | "title": "Fans", 39 | "type": "object", 40 | "additionalProperties": false, 41 | "properties": { 42 | "manual_offset": { 43 | "type": "number", 44 | "description": "风扇自动控制ec地址" 45 | }, 46 | "rpmwrite_offset": { 47 | "type": "number", 48 | "description": "风扇写入转速ec地址" 49 | }, 50 | "rpmread_offset": { 51 | "type": "number", 52 | "description": "风扇读取转速ec地址" 53 | }, 54 | "ram_reg_addr": { 55 | "type": "number", 56 | "description": "风扇ecRam寄存器地址" 57 | }, 58 | "ram_reg_data": { 59 | "type": "number", 60 | "description": "风扇ecRam寄存器数据" 61 | }, 62 | "ram_manual_offset": { 63 | "type": "number", 64 | "description": "风扇ecRam手动控制地址" 65 | }, 66 | "ram_rpmwrite_offset": { 67 | "type": "number", 68 | "description": "风扇ecRam写入转速地址" 69 | }, 70 | "ram_rpmread_offset": { 71 | "type": "number", 72 | "description": "风扇ecRam读取转速地址" 73 | }, 74 | "ram_rpmread_length": { 75 | "type": "number", 76 | "description": "风扇ecRam读取转速数据长度, 0为需要通过计算获得转速" 77 | }, 78 | "rpm_write_max": { 79 | "type": "number", 80 | "description": "风扇最大转速ec写入值" 81 | }, 82 | "rpm_value_max": { 83 | "type": "number", 84 | "description": "风扇最大转速值" 85 | }, 86 | "enable_manual_value": { 87 | "type": "number", 88 | "description": "风扇手动控制写入值, 默认 1" 89 | }, 90 | "enable_auto_value": { 91 | "type": "number", 92 | "description": "风扇自动控制写入值 , 默认 0" 93 | } 94 | }, 95 | "required": [] 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /py_modules/fan_config/schema/hwmon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$ref": "#/definitions/HWMONProfile", 4 | "definitions": { 5 | "HWMONProfile": { 6 | "title": "HWMONProfile", 7 | "type": "object", 8 | "additionalProperties": false, 9 | "properties": { 10 | "hwmon_name": { 11 | "type": "string", 12 | "description": "hwmon 目录下 name 文件内容" 13 | }, 14 | "fans": { 15 | "type": "array", 16 | "items": { 17 | "$ref": "#/definitions/Fans" 18 | }, 19 | "minItems": 1, 20 | "description": "风扇配置信息" 21 | } 22 | }, 23 | "required": ["hwmon_name", "fans"] 24 | }, 25 | "Fans": { 26 | "title": "Fans", 27 | "type": "object", 28 | "additionalProperties": false, 29 | "properties": { 30 | "fan_name": { 31 | "type": "string", 32 | "description": "显示在 UI 上的风扇名称" 33 | }, 34 | "pwm_mode": { 35 | "type": "number", 36 | "enum": [0, 1, 2], 37 | "description": "写入的模式 0.普通模式(对单个文件写入) 1.对多个文件写入同样的数值 2.对多个文件写入不同的数值" 38 | }, 39 | "black_list": { 40 | "type": "array", 41 | "items": { 42 | "type": "string" 43 | }, 44 | "description": "黑名单, 匹配 /sys/devices/virtual/dmi/id/product_name. 不使用该配置" 45 | }, 46 | "temp_mode": { 47 | "type": "number", 48 | "enum": [0, 1], 49 | "description": "温度使用哪个数据 0.cpu温度(不可用时切换到gpu温度) 1.gpu温度(不可用时切换到cpu温度)" 50 | }, 51 | "pwm_enable": { 52 | "$ref": "#/definitions/PWMEnable", 53 | "description": "PWMEnable 配置信息" 54 | }, 55 | "pwm_input": { 56 | "$ref": "#/definitions/PWMInput", 57 | "description": "PWMInput 配置信息" 58 | }, 59 | "pwm_write": { 60 | "$ref": "#/definitions/PWMWrite", 61 | "description": "PWMWrite 配置信息" 62 | } 63 | }, 64 | "required": [] 65 | }, 66 | "PWMEnable": { 67 | "title": "PWMEnable", 68 | "type": "object", 69 | "additionalProperties": false, 70 | "properties": { 71 | "manual_value": { 72 | "type": "number", 73 | "description": "手动模式写入的值" 74 | }, 75 | "auto_value": { 76 | "type": "number", 77 | "description": "自动模式写入的值" 78 | }, 79 | "pwm_enable_path": { 80 | "type": "string", 81 | "description": "写入数值文件路径" 82 | }, 83 | "pwm_enable_second_path": { 84 | "type": "string", 85 | "description": "写入数值文件路径(次选, pwm_enable_path 不存在时使用该路径)" 86 | } 87 | }, 88 | "required": ["manual_value", "auto_value", "pwm_enable_path"] 89 | }, 90 | "PWMInput": { 91 | "title": "PWMInput", 92 | "type": "object", 93 | "additionalProperties": false, 94 | "properties": { 95 | "hwmon_label": { 96 | "type": "string", 97 | "description": "读转速的hwmon标签(读取转速和写入转速可能不在同一个hwmon)" 98 | }, 99 | "pwm_read_path": { 100 | "type": "string", 101 | "description": "读取数值文件路径" 102 | }, 103 | "pwm_read_max": { 104 | "type": "number", 105 | "description": "读取数值最大值" 106 | } 107 | }, 108 | "required": ["pwm_read_path", "pwm_read_max"] 109 | }, 110 | "PWMWrite": { 111 | "title": "PWMWrite", 112 | "type": "object", 113 | "additionalProperties": false, 114 | "properties": { 115 | "pwm_write_max": { 116 | "type": "object", 117 | "additionalProperties": { 118 | "type": "number" 119 | }, 120 | "required": ["default"], 121 | "description": "写入转速最大值(根据不同产品名写入不同的最大值,没有则使用默认)" 122 | }, 123 | "pwm_write_path": { 124 | "type": "string", 125 | "description": "写入数值文件路径" 126 | }, 127 | "default_curve": { 128 | "$ref": "#/definitions/PWMWriteDefaultCurve", 129 | "description": "默认曲线" 130 | }, 131 | "curve_path": { 132 | "$ref": "#/definitions/PWMWriteCurvePath", 133 | "description": "曲线路径" 134 | } 135 | }, 136 | "required": [] 137 | }, 138 | "PWMWriteDefaultCurve": { 139 | "title": "PWMWriteDefaultCurve", 140 | "type": "object", 141 | "additionalProperties": false, 142 | "properties": { 143 | "temp": { 144 | "type": "array", 145 | "items": { 146 | "type": "number" 147 | }, 148 | "description": "温度数组" 149 | }, 150 | "speed": { 151 | "type": "array", 152 | "items": { 153 | "type": "number" 154 | }, 155 | "description": "转速数组" 156 | }, 157 | "pwm": { 158 | "type": "array", 159 | "items": { 160 | "type": "number" 161 | }, 162 | "description": "PWM数组" 163 | } 164 | }, 165 | "required": ["temp", "pwm"] 166 | }, 167 | "PWMWriteCurvePath": { 168 | "title": "PWMWriteCurvePath", 169 | "type": "object", 170 | "additionalProperties": false, 171 | "properties": { 172 | "temp_write": { 173 | "type": "array", 174 | "items": { 175 | "type": "string" 176 | }, 177 | "description": "温度路径数组" 178 | }, 179 | "pwm_write": { 180 | "type": "array", 181 | "items": { 182 | "type": "string" 183 | }, 184 | "description": "PWM路径数组" 185 | } 186 | }, 187 | "required": ["temp_write", "pwm_write"] 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /py_modules/fuse_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import threading 4 | from threading import Event 5 | 6 | import decky 7 | from conf_manager import confManager 8 | from config import logger 9 | from power_manager import PowerManager 10 | 11 | 12 | class FuseManager: 13 | """ 14 | FUSE 管理器单例类 15 | 确保整个应用程序中只有一个 FUSE 管理器实例,避免资源冲突 16 | """ 17 | 18 | _instance = None 19 | _lock = threading.Lock() # 确保线程安全的锁 20 | 21 | @classmethod 22 | def get_instance(cls, max_tdp=30, power_manager=None): 23 | """ 24 | 获取 FuseManager 单例实例 25 | 如果实例不存在,则创建一个新实例 26 | 27 | Args: 28 | max_tdp: 最大 TDP 值 29 | power_manager: 电源管理器实例 30 | 31 | Returns: 32 | FuseManager 单例实例 33 | """ 34 | with cls._lock: 35 | if cls._instance is None: 36 | logger.info("Creating new FuseManager instance") 37 | cls._instance = cls(max_tdp, power_manager) 38 | else: 39 | # 更新现有实例的属性 40 | if power_manager and not cls._instance.power_manager: 41 | logger.info("Updating FuseManager power_manager") 42 | cls._instance.power_manager = power_manager 43 | 44 | if max_tdp != cls._instance.max_tdp: 45 | logger.info( 46 | f"Updating FuseManager max_tdp from {cls._instance.max_tdp} to {max_tdp}" 47 | ) 48 | cls._instance.max_tdp = max_tdp 49 | 50 | return cls._instance 51 | 52 | def __init__( 53 | self, 54 | max_tdp=30, 55 | power_manager: PowerManager = None, 56 | ): 57 | """ 58 | 初始化 FuseManager 59 | 注意:不应直接调用此构造函数,而是使用 get_instance 方法 60 | """ 61 | # 如果已经有实例,则直接返回 62 | if FuseManager._instance is not None: 63 | logger.warning( 64 | "FuseManager singleton already exists - use get_instance() instead" 65 | ) 66 | return 67 | 68 | self.t = None 69 | self.t_sys = None 70 | self.should_exit = None 71 | self.emit = None 72 | self.min_tdp = 3 73 | self.default_tdp = 500 74 | self.max_tdp = max_tdp 75 | self.power_manager = power_manager 76 | self.igpu_path = None 77 | self._initialized = False 78 | logger.info(f"FuseManager initialized with max_tdp={max_tdp}") 79 | 80 | def __del__(self): 81 | """ 82 | 在对象被销毁时执行清理操作 83 | """ 84 | self.unload() 85 | 86 | def unload(self): 87 | """ 88 | 清理 FUSE 相关资源 89 | 1. 设置退出标志 90 | 2. 等待线程结束 91 | 3. 卸载 FUSE 挂载点 92 | 4. 清理 socket 文件 93 | """ 94 | # 如果没有初始化,则无需执行清理 95 | if not self._initialized: 96 | return 97 | 98 | try: 99 | # 1. 设置退出标志 100 | if self.should_exit: 101 | self.should_exit.set() 102 | logger.info("Set exit flag for TDP client") 103 | 104 | # 2. 等待线程结束 105 | if self.t_sys and self.t_sys.is_alive(): 106 | logger.info("Waiting for TDP client thread to exit") 107 | self.t_sys.join(timeout=5) # 设置超时时间,避免永久等待 108 | if self.t_sys.is_alive(): 109 | logger.warning("TDP client thread did not exit within timeout") 110 | 111 | # 3. 卸载 FUSE 挂载点 112 | if self.igpu_path: 113 | try: 114 | from pfuse import umount_fuse_igpu 115 | 116 | logger.info("Attempting to unmount FUSE mount points") 117 | if not umount_fuse_igpu(): 118 | logger.error("Failed to unmount FUSE mount points") 119 | except Exception as e: 120 | logger.error(f"Error during FUSE unmount: {str(e)}", exc_info=True) 121 | 122 | # 4. 清理 socket 文件 123 | socket_path = "/run/powercontrol/socket" 124 | if os.path.exists(socket_path): 125 | try: 126 | os.remove(socket_path) 127 | logger.info("Cleaned up socket file") 128 | except Exception as e: 129 | logger.error( 130 | f"Failed to remove socket file: {str(e)}", exc_info=True 131 | ) 132 | 133 | except Exception as e: 134 | logger.error(f"Error during FUSE cleanup: {str(e)}", exc_info=True) 135 | finally: 136 | # 确保所有引用都被清理 137 | self.t_sys = None 138 | self.should_exit = None 139 | self.igpu_path = None 140 | self._initialized = False 141 | 142 | def emit_tdp_frontend(self, tdp): 143 | """ 144 | 发送 tdp 到前端 145 | 不直接调用 tdp 控制函数,而是通过事件通知前端 146 | 再由前端处理后,最终调用 tdp 控制函数 147 | 否则无法实现UI和插件同步 148 | """ 149 | logger.info(f"QAM Set TDPLimit: {tdp}") 150 | asyncio.run(decky.emit("QAM_setTDP", tdp)) 151 | 152 | def apply_tdp(self, tdp): 153 | """ 154 | 直接应用TDP 155 | """ 156 | logger.info(f"Apply TDP: {tdp}") 157 | if tdp == self.default_tdp: 158 | logger.info(f"Apply TDP: default, set to max {self.max_tdp}") 159 | tdp = self.max_tdp 160 | if self.power_manager: 161 | self.power_manager.set_tdp(tdp) 162 | else: 163 | logger.error("power_manager is not set") 164 | 165 | def fuse_init(self): 166 | """ 167 | 初始化 fuse, 使用 fuse 处理 hwmon, 使得 steam 可以通过侧边栏调整 tdp 168 | 在 TDP 控制开关关闭后, steam 默认会设置 TDP 为 default_tdp 169 | 170 | 目前问题: 171 | 对于 tdp 控制条最大最小值的调整, 需要重启 steam 才能生效 172 | 并且 暂时找不到办法去更新控制条UI的值。导致如果因电源策略或者应用策略导致 tdp 变化 173 | 会不能将值应用到侧边栏的 tdp 控制条 174 | 175 | 简而言之 就是侧边栏的调整能同步到插件中,但是插件中的调整不能同步到侧边栏 176 | """ 177 | # 如果已经初始化,则返回 178 | if self._initialized: 179 | logger.info("FuseManager already initialized") 180 | return True 181 | 182 | from pfuse import ( 183 | find_igpu, 184 | prepare_tdp_mount, 185 | start_tdp_client, 186 | umount_fuse_igpu, 187 | ) 188 | from utils.tdp import getMaxTDP 189 | 190 | umount_fuse_igpu() 191 | 192 | # find igpu 193 | self.igpu_path = find_igpu() 194 | if not self.igpu_path: 195 | logger.error("No iGPU found, cannot initialize FUSE") 196 | return False 197 | 198 | settings = confManager.getSettings() 199 | tdpMax = getMaxTDP() 200 | enableCustomTDPRange = settings.get("enableCustomTDPRange", False) 201 | realTDPMax = ( 202 | settings.get("customTDPRangeMax", tdpMax) 203 | if enableCustomTDPRange 204 | else tdpMax 205 | ) 206 | 207 | logger.info(f">>>> FuseManager tdpMax: {realTDPMax}") 208 | self.max_tdp = realTDPMax 209 | 210 | if self.should_exit: 211 | return False 212 | 213 | self.should_exit = Event() 214 | try: 215 | stat = prepare_tdp_mount() 216 | if stat: 217 | self.t_sys = start_tdp_client( 218 | self.should_exit, 219 | self.apply_tdp, 220 | self.min_tdp, 221 | self.default_tdp, 222 | self.max_tdp, 223 | ) 224 | self._initialized = True 225 | logger.info("FuseManager successfully initialized") 226 | return True 227 | else: 228 | logger.error("Failed to prepare TDP mount") 229 | return False 230 | except Exception as e: 231 | logger.error(f"Failed to start FUSE: {str(e)}", exc_info=True) 232 | return False 233 | -------------------------------------------------------------------------------- /py_modules/inotify.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import os 3 | import struct 4 | import sys 5 | from ctypes import CDLL, get_errno 6 | from threading import Thread, Timer 7 | 8 | from config import logger 9 | 10 | IN_ACCESS = 0x00000001 # 文件被访问 11 | IN_MODIFY = 0x00000002 # 文件被修改 12 | IN_ATTRIB = 0x00000004 # 元数据改变 13 | IN_CLOSE_WRITE = 0x00000008 # 可写文件被关闭 14 | IN_CLOSE_NOWRITE = 0x00000010 # 不可写文件被关闭 15 | IN_OPEN = 0x00000020 # 文件被打开 16 | IN_MOVED_FROM = 0x00000040 # 文件从X移动 17 | IN_MOVED_TO = 0x00000080 # 文件被移动到Y 18 | IN_CREATE = 0x00000100 # 子文件创建 19 | IN_DELETE = 0x00000200 # 子文件删除 20 | IN_DELETE_SELF = 0x00000400 # 自身(被监视的项本身)被删除 21 | IN_MOVE_SELF = 0x00000800 # 自身(被监视的项本身)被移动 22 | 23 | 24 | class Inotify: 25 | def __init__(self): 26 | try: 27 | self._libc = CDLL(None, use_errno=True) 28 | self._libc.inotify_init.argtypes = [] 29 | self._libc.inotify_init.restype = ctypes.c_int 30 | self._libc.inotify_add_watch.argtypes = [ 31 | ctypes.c_int, 32 | ctypes.c_char_p, 33 | ctypes.c_uint32, 34 | ] 35 | self._libc.inotify_add_watch.restype = ctypes.c_int 36 | self._libc.inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int] 37 | self._libc.inotify_rm_watch.restype = ctypes.c_int 38 | 39 | self.fd = self._libc.inotify_init() 40 | 41 | self._wdMap = {} 42 | self._delay = 0.5 43 | self._delaytimer = {} 44 | self._runThread = None 45 | except Exception as e: 46 | logger.error(e) 47 | 48 | def _process(self): 49 | try: 50 | if self._runThread: 51 | nowThread = self._runThread.name 52 | while self._runThread and self._runThread.name == nowThread: 53 | buf = os.read(self.fd, 4096) 54 | pos = 0 55 | wdMap = self._wdMap.copy() 56 | while pos < len(buf): 57 | (wd, mask, cookie, name_len) = struct.unpack( 58 | "iIII", buf[pos : pos + 16] 59 | ) 60 | pos += 16 61 | (name,) = struct.unpack("%ds" % name_len, buf[pos : pos + name_len]) 62 | pos += name_len 63 | item = wdMap.get(wd) 64 | if item and self._runThread and self._runThread.name == nowThread: 65 | self._delayCall(wd, item["callback"], item["path"], mask) 66 | except Exception as e: 67 | logger.error(e) 68 | 69 | def _delayCall(self, wd, callfunc, *args): 70 | try: 71 | if ( 72 | wd in self._delaytimer 73 | and self._delaytimer[wd] is not None 74 | and self._delaytimer[wd].is_alive() 75 | ): 76 | self._delaytimer[wd].cancel() 77 | self._delaytimer[wd] = Timer( 78 | self._delay, lambda: self._onCallBack(callfunc, *args) 79 | ) 80 | self._delaytimer[wd].start() 81 | except Exception as e: 82 | logger.error(e) 83 | 84 | def _onCallBack(self, callfunc, *args): 85 | logger.debug(f"callback path:{args[0]}, mask:{args[1]}") 86 | callfunc(*args) 87 | 88 | def add_watch(self, path, mask, callback): 89 | try: 90 | logger.debug( 91 | f"add_watch(self, path:{path}, mask:{mask}, callback:{callback})" 92 | ) 93 | path_buf = ctypes.create_string_buffer( 94 | path.encode(sys.getfilesystemencoding()) 95 | ) 96 | wd = self._libc.inotify_add_watch(self.fd, path_buf, mask) 97 | self._wdMap[wd] = {"path": path, "callback": callback} 98 | if wd < 0: 99 | sys.stderr.write( 100 | f"can't add watch for {path_buf}: {os.strerror(get_errno())}\n" 101 | ) 102 | return wd 103 | except Exception as e: 104 | logger.error(e) 105 | 106 | def remove_watch(self, path): 107 | try: 108 | for wd in list(self._wdMap): 109 | if path == self._wdMap[wd]["path"]: 110 | if self._libc.inotify_rm_watch(self.fd, wd) < 0: 111 | sys.stderr.write( 112 | f"can't remove watch: {os.strerror(get_errno())}\n" 113 | ) 114 | else: 115 | self._wdMap.pop(wd) 116 | except Exception as e: 117 | logger.error(e) 118 | 119 | def run(self): 120 | try: 121 | if self._runThread: 122 | pass 123 | else: 124 | self._runThread = Thread(target=self._process) 125 | self._runThread.start() 126 | except Exception as e: 127 | logger.error(e) 128 | 129 | def stop(self): 130 | self._runThread = None 131 | 132 | 133 | notify = Inotify() 134 | notify.run() 135 | -------------------------------------------------------------------------------- /py_modules/logging_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import subprocess 4 | 5 | LOG = "/tmp/PowerControl_systemd.log" 6 | LOG_TAG = "powercontrol" 7 | 8 | 9 | class SystemdHandler(logging.Handler): 10 | PRIORITY_MAP = { 11 | logging.DEBUG: "7", # debug 12 | logging.INFO: "6", # info 13 | logging.WARNING: "4", # warning 14 | logging.ERROR: "3", # err 15 | logging.CRITICAL: "2", # crit 16 | } 17 | 18 | def emit(self, record): 19 | msg = self.format(record) 20 | priority = self.PRIORITY_MAP.get(record.levelno, "6") 21 | try: 22 | # 使用系统的 systemd-cat 23 | env = os.environ.copy() 24 | env["LD_LIBRARY_PATH"] = "" # 清除 LD_LIBRARY_PATH 25 | subprocess.run( 26 | ["systemd-cat", "-t", LOG_TAG, "-p", priority], 27 | input=msg, 28 | text=True, 29 | env=env, 30 | ) 31 | except Exception as e: 32 | self.write_log(f"systemd-cat error: {e}") 33 | 34 | def write_log(self, msg): 35 | try: 36 | with open(LOG, "a") as f: 37 | f.write(msg) 38 | f.write("\n") 39 | except Exception as e: 40 | print(e) 41 | -------------------------------------------------------------------------------- /py_modules/pfuse/README.md: -------------------------------------------------------------------------------- 1 | # fuse 2 | 3 | copy from [adjustor](https://github.com/hhd-dev/adjustor) 4 | -------------------------------------------------------------------------------- /py_modules/pfuse/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import find_igpu, prepare_tdp_mount, start_tdp_client, umount_fuse_igpu 2 | 3 | __all__ = ["start_tdp_client", "prepare_tdp_mount", "find_igpu", "umount_fuse_igpu"] 4 | -------------------------------------------------------------------------------- /py_modules/portio.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mengmeet/PowerControl/0157e2e41d63d8e308e16abd6244df94cc68fdb8/py_modules/portio.so -------------------------------------------------------------------------------- /py_modules/power_manager.py: -------------------------------------------------------------------------------- 1 | from config import logger 2 | from devices import IDevice 3 | 4 | 5 | class PowerManager: 6 | def __init__(self): 7 | self._device = IDevice.get_current() 8 | logger.info(f"当前使用的设备类型: {type(self._device)}") 9 | self._device.load() 10 | 11 | def __getattr__(self, name): 12 | """动态委托到设备实例""" 13 | return getattr(self._device, name) 14 | 15 | def __del__(self): 16 | self._device.unload() 17 | -------------------------------------------------------------------------------- /py_modules/tt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WRITE_PATH="$1" 4 | WRITE_VALUE="$2" 5 | 6 | if [[ "$WRITE_PATH" == /sys/class/drm/card*/device/power_dpm_force_performance_level ]]; then 7 | GPU=$(grep -H 0x8086 /sys/class/drm/card?/device/vendor 2>/dev/null | head -n1 | sed 's/\/device\/vendor:.*//') 8 | GPU_MIN_FREQ="$GPU/gt_min_freq_mhz" 9 | GPU_MAX_FREQ="$GPU/gt_max_freq_mhz" 10 | GPU_MIN_LIMIT="$(cat $GPU/gt_RPn_freq_mhz)" 11 | GPU_MAX_LIMIT="$(cat $GPU/gt_RP0_freq_mhz)" 12 | echo "setting intel gpu $GPU to [$WRITE_VALUE]" | systemd-cat -t p-steamos-priv-write -p warning 13 | if [[ "$WRITE_VALUE" == "auto" ]]; then 14 | echo "$GPU_MIN_LIMIT" >"$GPU_MIN_FREQ" 15 | echo "$GPU_MAX_LIMIT" >"$GPU_MAX_FREQ" 16 | echo "commit: $GPU_MIN_LIMIT -> $GPU_MIN_FREQ" | systemd-cat -t p-steamos-priv-write -p warning 17 | echo "commit: $GPU_MAX_LIMIT -> $GPU_MAX_FREQ" | systemd-cat -t p-steamos-priv-write -p warning 18 | fi 19 | exit 0 20 | fi 21 | 22 | if [[ "$WRITE_PATH" == /sys/class/drm/card*/device/pp_od_clk_voltage ]]; then 23 | GPU=$(grep -H 0x8086 /sys/class/drm/card?/device/vendor 2>/dev/null | head -n1 | sed 's/\/device\/vendor:.*//') 24 | GPU_MIN_FREQ="$GPU/gt_min_freq_mhz" 25 | GPU_MAX_FREQ="$GPU/gt_max_freq_mhz" 26 | GPU_MIN_LIMIT="$(cat $GPU/gt_RPn_freq_mhz)" 27 | GPU_MAX_LIMIT="$(cat $GPU/gt_RP0_freq_mhz)" 28 | echo "commit: GPU -> $WRITE_VALUE" | systemd-cat -t p-steamos-priv-write -p warning 29 | if [[ "$WRITE_VALUE" =~ "s 0" ]]; then 30 | min_freq=$(echo "$WRITE_VALUE" | sed 's/.*s 0 //') 31 | if [[ "$(cat $GPU_MAX_FREQ)" -lt "$min_freq" ]]; then 32 | echo "commit: $GPU_MAX_FREQ -> $min_freq" | systemd-cat -t p-steamos-priv-write -p warning 33 | echo "$min_freq" >"$GPU_MAX_FREQ" 34 | fi 35 | if [[ "$min_freq" -lt "$GPU_MIN_LIMIT" ]]; then 36 | min_freq="$GPU_MIN_LIMIT" 37 | fi 38 | if [[ "$min_freq" -gt "$GPU_MAX_LIMIT" ]]; then 39 | min_freq="$GPU_MIN_LIMIT" 40 | fi 41 | echo "commit: $min_freq -> $GPU_MIN_FREQ" | systemd-cat -t p-steamos-priv-write -p warning 42 | echo "$min_freq" >"$GPU_MIN_FREQ" 43 | fi 44 | if [[ "$WRITE_VALUE" =~ "s 1" ]]; then 45 | max_freq=$(echo "$WRITE_VALUE" | sed 's/.*s 1 //') 46 | if [[ "$max_freq" -gt "$GPU_MAX_LIMIT" ]]; then 47 | max_freq="$GPU_MAX_LIMIT" 48 | fi 49 | echo "commit: $max_freq -> $GPU_MAX_FREQ" | systemd-cat -t p-steamos-priv-write -p warning 50 | echo "$max_freq" >"$GPU_MAX_FREQ" 51 | fi 52 | exit 0 53 | fi 54 | -------------------------------------------------------------------------------- /py_modules/update.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import ssl 5 | import stat 6 | import subprocess 7 | import urllib.request 8 | 9 | import decky 10 | from config import API_URL, logger 11 | from fuse_manager import FuseManager 12 | from utils import get_env 13 | 14 | 15 | def recursive_chmod(path, perms): 16 | for dirpath, dirnames, filenames in os.walk(path): 17 | current_perms = os.stat(dirpath).st_mode 18 | os.chmod(dirpath, current_perms | perms) 19 | for filename in filenames: 20 | os.chmod(os.path.join(dirpath, filename), current_perms | perms) 21 | 22 | 23 | def update_latest(): 24 | downloaded_filepath = download_latest_build() 25 | 26 | if os.path.exists(downloaded_filepath): 27 | plugin_dir = decky.DECKY_PLUGIN_DIR 28 | 29 | try: 30 | logger.info(f"removing old plugin from {plugin_dir}") 31 | # add write perms to directory 32 | recursive_chmod(plugin_dir, stat.S_IWUSR) 33 | 34 | # remove old plugin 35 | shutil.rmtree(plugin_dir) 36 | except Exception as e: 37 | logger.error(f"ota error during removal of old plugin: {e}") 38 | 39 | try: 40 | logger.info(f"extracting ota file to {plugin_dir}") 41 | # extract files to decky plugins dir 42 | shutil.unpack_archive( 43 | downloaded_filepath, 44 | f"{decky.DECKY_USER_HOME}/homebrew/plugins", 45 | format="gztar" if downloaded_filepath.endswith(".tar.gz") else "zip", 46 | ) 47 | 48 | # cleanup downloaded files 49 | os.remove(downloaded_filepath) 50 | except Exception as e: 51 | decky.logger.error(f"error during ota file extraction {e}") 52 | 53 | logger.info("restarting plugin_loader") 54 | # cmd = "systemctl restart plugin_loader.service" 55 | cmd = "pkill -HUP PluginLoader" 56 | FuseManager.get_instance().unload() 57 | result = subprocess.run( 58 | cmd, 59 | shell=True, 60 | check=True, 61 | text=True, 62 | stdout=subprocess.PIPE, 63 | stderr=subprocess.PIPE, 64 | env=get_env(), 65 | ) 66 | logger.info(result.stdout) 67 | return result 68 | 69 | 70 | def download_latest_build(): 71 | gcontext = ssl.SSLContext() 72 | 73 | response = urllib.request.urlopen(API_URL, context=gcontext) 74 | json_data = json.load(response) 75 | 76 | download_url = json_data.get("assets")[0].get("browser_download_url") 77 | 78 | logger.info(download_url) 79 | 80 | if download_url.endswith(".zip"): 81 | file_path = f"/tmp/{decky.DECKY_PLUGIN_NAME}.zip" 82 | else: 83 | file_path = f"/tmp/{decky.DECKY_PLUGIN_NAME}.tar.gz" 84 | 85 | with ( 86 | urllib.request.urlopen(download_url, context=gcontext) as response, 87 | open(file_path, "wb") as output_file, 88 | ): 89 | output_file.write(response.read()) 90 | output_file.close() 91 | 92 | return file_path 93 | 94 | 95 | def get_version(): 96 | return f"{decky.DECKY_PLUGIN_VERSION}" 97 | 98 | 99 | def get_latest_version(): 100 | gcontext = ssl.SSLContext() 101 | 102 | response = urllib.request.urlopen(API_URL, context=gcontext) 103 | json_data = json.load(response) 104 | 105 | tag = json_data.get("tag_name") 106 | # if tag is a v* tag, remove the v 107 | if tag.startswith("v"): 108 | tag = tag[1:] 109 | return tag 110 | -------------------------------------------------------------------------------- /py_modules/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | import signal 4 | from contextlib import contextmanager 5 | 6 | from .battery import ( 7 | get_battery_info, 8 | get_battery_percentage, 9 | get_charge_behaviour, 10 | get_charge_control_end_threshold, 11 | get_charge_type, 12 | is_battery_charging, 13 | set_charge_behaviour, 14 | set_charge_control_end_threshold, 15 | set_charge_type, 16 | support_charge_behaviour, 17 | support_charge_control_end_threshold, 18 | support_charge_type, 19 | ) 20 | from .gpu_fix import fix_gpuFreqSlider_AMD, fix_gpuFreqSlider_INTEL 21 | from .tdp import getMaxTDP 22 | 23 | __all__ = [ 24 | "fix_gpuFreqSlider_AMD", 25 | "fix_gpuFreqSlider_INTEL", 26 | "getMaxTDP", 27 | "get_env", 28 | "get_battery_info", 29 | "get_battery_percentage", 30 | "is_battery_charging", 31 | "support_charge_control_end_threshold", 32 | "set_charge_control_end_threshold", 33 | "support_charge_behaviour", 34 | "set_charge_behaviour", 35 | "get_charge_behaviour", 36 | "get_charge_control_end_threshold", 37 | "support_charge_type", 38 | "set_charge_type", 39 | "get_charge_type", 40 | "version_compare", 41 | ] 42 | 43 | 44 | def get_env(): 45 | env = os.environ.copy() 46 | env["LD_LIBRARY_PATH"] = "" 47 | return env 48 | 49 | 50 | # 版本号对比 数组参数 51 | def version_compare(version1: List[int], version2: List[int]) -> int: 52 | """ 53 | 比较两个版本号数组的大小 54 | 55 | Args: 56 | version1 (list): 第一个版本号数组,如 [1, 2, 3] 57 | version2 (list): 第二个版本号数组,如 [1, 2, 4] 58 | 59 | Returns: 60 | int: 如果 version1 > version2 返回 1,如果 version1 < version2 返回 -1,如果相等返回 0 61 | """ 62 | # 获取两个版本号数组的长度 63 | len1, len2 = len(version1), len(version2) 64 | 65 | # 取较短的长度进行比较 66 | for i in range(min(len1, len2)): 67 | if version1[i] > version2[i]: 68 | return 1 69 | elif version1[i] < version2[i]: 70 | return -1 71 | 72 | # 如果前面的版本号都相同,比较长度 73 | if len1 > len2: 74 | return 1 75 | elif len1 < len2: 76 | return -1 77 | else: 78 | return 0 79 | 80 | 81 | class TimeoutException(Exception): 82 | pass 83 | 84 | 85 | @contextmanager 86 | def time_limit(seconds): 87 | def signal_handler(signum, frame): 88 | raise TimeoutException("Timed out!") 89 | 90 | signal.signal(signal.SIGALRM, signal_handler) 91 | signal.alarm(seconds) 92 | try: 93 | yield 94 | finally: 95 | signal.alarm(0) 96 | 97 | 98 | def get_bios_settings(): 99 | import json 100 | import subprocess 101 | from config import logger 102 | 103 | try: 104 | cmd = "fwupdmgr get-bios-setting --json" 105 | result = subprocess.run( 106 | cmd, 107 | shell=True, 108 | check=True, 109 | text=True, 110 | stdout=subprocess.PIPE, 111 | stderr=subprocess.PIPE, 112 | timeout=2, 113 | env=get_env(), 114 | ) 115 | 116 | data = json.loads(result.stdout) 117 | return data 118 | except subprocess.TimeoutExpired: 119 | logger.error("Timeout when getting BIOS settings") 120 | return {"BiosSettings": []} 121 | except Exception as e: 122 | logger.error(f"Error get_bios_setting {e}") 123 | return {"BiosSettings": []} 124 | -------------------------------------------------------------------------------- /py_modules/utils/tdp.py: -------------------------------------------------------------------------------- 1 | from config import ( 2 | CPU_ID, 3 | PRODUCT_NAME, 4 | TDP_LIMIT_CONFIG_CPU, 5 | TDP_LIMIT_CONFIG_PRODUCT, 6 | logger, 7 | ) 8 | 9 | 10 | def getMaxTDP(default: int = 15) -> int: 11 | """获取最大TDP值。 12 | 13 | Returns: 14 | int: 最大TDP值(瓦特) 15 | """ 16 | try: 17 | # 根据机器型号或者CPU型号返回tdp最大值 18 | if PRODUCT_NAME in TDP_LIMIT_CONFIG_PRODUCT: 19 | cpu_tdpMax = TDP_LIMIT_CONFIG_PRODUCT[PRODUCT_NAME] 20 | else: 21 | for model in TDP_LIMIT_CONFIG_CPU: 22 | if model in CPU_ID: 23 | cpu_tdpMax = TDP_LIMIT_CONFIG_CPU[model] 24 | break 25 | else: 26 | cpu_tdpMax = default 27 | logger.info("getMaxTDP {}".format(cpu_tdpMax)) 28 | return cpu_tdpMax 29 | except Exception: 30 | logger.error("Failed to get max TDP value", exc_info=True) 31 | return default 32 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import deckyPlugin from "@decky/rollup"; 2 | import { mergeAndConcat } from "merge-anything"; 3 | 4 | const userOptions = { 5 | // Add your extra Rollup options here 6 | treeshake: { 7 | preset: "recommended", 8 | }, 9 | }; 10 | 11 | // must be merged in the order of default options first, then user options 12 | export default mergeAndConcat(deckyPlugin(), userOptions); 13 | -------------------------------------------------------------------------------- /src/components/SlowSliderField.tsx: -------------------------------------------------------------------------------- 1 | import { NotchLabel, SliderField } from "@decky/ui"; 2 | import { ItemProps } from "@decky/ui/dist/components/Item"; 3 | import { useEffect, useRef } from "react"; 4 | import { useState } from "react"; 5 | import { FC } from "react"; 6 | 7 | export interface SlowSliderFieldProps extends ItemProps { 8 | value: number; 9 | min?: number; 10 | max?: number; 11 | changeMin?: number; 12 | changeMax?: number; 13 | step?: number; 14 | notchCount?: number; 15 | notchLabels?: NotchLabel[]; 16 | notchTicksVisible?: boolean; 17 | showValue?: boolean; 18 | resetValue?: number; 19 | disabled?: boolean; 20 | editableValue?: boolean; 21 | validValues?: "steps" | "range" | ((value: number) => boolean); 22 | valueSuffix?: string; 23 | minimumDpadGranularity?: number; 24 | onChange?(value: number): void; 25 | onChangeEnd?(value: number): void; 26 | } 27 | export const SlowSliderField: FC = (slider) => { 28 | const [changeValue, SetChangeValue] = useState(slider.value); 29 | const isChanging = useRef(false); 30 | useEffect(() => { 31 | setTimeout(() => { 32 | //console.debug("changeValue=",changeValue,"slider=",slider.value) 33 | if (changeValue == slider.value) { 34 | slider.onChangeEnd?.call(slider, slider.value); 35 | isChanging.current = false; 36 | } 37 | }, 500); 38 | }, [changeValue]); 39 | return ( 40 | { 58 | var tpvalue = value; 59 | if (slider.changeMax != undefined) 60 | tpvalue = slider.changeMax <= value ? slider.changeMax : value; 61 | if (slider.changeMin != undefined) 62 | tpvalue = slider.changeMin >= value ? slider.changeMin : value; 63 | isChanging.current = true; 64 | slider.onChange?.call(slider, tpvalue); 65 | slider.value = tpvalue; 66 | SetChangeValue(tpvalue); 67 | }} 68 | /> 69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/actionButtonItem.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonItem, ButtonItemProps, Spinner } from "@decky/ui"; 2 | import { FC, useState } from "react"; 3 | 4 | export interface ActionButtonItemProps extends ButtonItemProps { 5 | loading?: boolean; 6 | debugLabel?: string; 7 | } 8 | 9 | export const ActionButtonItem: FC = (props) => { 10 | const { onClick, disabled, children, loading, layout, debugLabel } = props; 11 | 12 | const [_loading, setLoading] = useState(loading); 13 | 14 | const handClick = async ( 15 | event: MouseEvent, 16 | onClick?: (e: MouseEvent) => void 17 | ) => { 18 | try { 19 | console.log(`ActionButtonItem: ${debugLabel}`); 20 | setLoading(true); 21 | await onClick?.(event); 22 | console.log(`ActionButtonItem: ${debugLabel} done`); 23 | } catch (e) { 24 | console.error(`ActionButtonItem error: ${e}`); 25 | } finally { 26 | // console.log(`ActionButtonItem: ${debugLabel} disable loading`); 27 | setLoading(false); 28 | } 29 | }; 30 | 31 | const isLoading = _loading; 32 | 33 | return ( 34 | handClick(e, onClick)} 39 | > 40 | 47 | {children}{" "} 48 | {isLoading && } 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/customTDP.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import { 3 | ComponentName, 4 | DEFAULT_TDP_MIN, 5 | PluginManager, 6 | Settings, 7 | UpdateType, 8 | } from "../util"; 9 | import { PanelSectionRow, SliderField, ToggleField } from "@decky/ui"; 10 | import { localizationManager, localizeStrEnum } from "../i18n"; 11 | 12 | export const CustomTDPComponent: FC = () => { 13 | const [show, setShow] = useState(Settings.ensureEnable()); 14 | const [enableCustomTDPRange, setEnableCustomTDPRange] = useState( 15 | Settings.appEnableCustomTDPRange() 16 | ); 17 | const [customTDPRangeMax, setCustomTDPRangeMax] = useState( 18 | Settings.appCustomTDPRangeMax() 19 | ); 20 | // const [customTDPRangeMin, setCustomTDPRangeMin] = useState(Settings.appCustomTDPRangeMin()); 21 | 22 | const hide = (ishide: boolean) => { 23 | setShow(!ishide); 24 | }; 25 | const refresh = () => { 26 | setEnableCustomTDPRange(Settings.appEnableCustomTDPRange()); 27 | setCustomTDPRangeMax(Settings.appCustomTDPRangeMax()); 28 | // setCustomTDPRangeMin(Settings.appCustomTDPRangeMin()); 29 | }; 30 | 31 | useEffect(() => { 32 | PluginManager.listenUpdateComponent( 33 | ComponentName.CUSTOM_TDP, 34 | [ComponentName.CPU_TDP, ComponentName.CUSTOM_TDP], 35 | (_ComponentName, updateType: string) => { 36 | switch (updateType) { 37 | case UpdateType.UPDATE: 38 | refresh(); 39 | break; 40 | case UpdateType.SHOW: 41 | hide(false); 42 | break; 43 | case UpdateType.HIDE: 44 | hide(true); 45 | break; 46 | } 47 | } 48 | ); 49 | }); 50 | 51 | const _sliderMin = DEFAULT_TDP_MIN; 52 | const _sliderMax = 100; 53 | 54 | return ( 55 |
56 | {show && ( 57 | 58 | { 64 | Settings.setEnableCustomTDPRange(val); 65 | }} 66 | /> 67 | 68 | )} 69 | {show && enableCustomTDPRange && ( 70 | 71 | { 80 | if (value > Settings.appCustomTDPRangeMin()) { 81 | Settings.setCustomTDPRangeMax(value); 82 | } 83 | }} 84 | /> 85 | 86 | )} 87 | {/* {show && enableCustomTDPRange && 88 | 89 | { 98 | if (value < Settings.appCustomTDPRangeMax()) { 99 | Settings.setCustomTDPRangeMin(value); 100 | } 101 | }} 102 | /> 103 | } */} 104 |
105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/components/fanCanvas.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { FC } from "react"; 3 | import { FanPosition } from "../util"; 4 | export interface FanCanvasProps { 5 | width: number; 6 | height: number; 7 | style: any; 8 | initDraw?(canvasRef: any): void; 9 | onPointerDown?(position: any): void; 10 | onPointerMove?(position: any): void; 11 | onPointerUp?(position: any): void; 12 | onPointerShortPress?(position: any): void; 13 | onPointerLongPress?(position: any): void; 14 | onPointerDragDown?(position: any): boolean; 15 | onPointerDraging?(position: any): void; 16 | } 17 | export const FanCanvas: FC = (canvas) => { 18 | const pointerDownPos: any = useRef(null); 19 | const pointerDownTime: any = useRef(null); 20 | const pointerUpPos: any = useRef(null); 21 | const pointerUpTime: any = useRef(null); 22 | const pointerIsDrag = useRef(false); 23 | const canvasRef: any = useRef(null); 24 | useEffect(() => { 25 | canvas.initDraw?.call(canvas, canvasRef.current); 26 | }, []); 27 | 28 | function getlayerXY(e: any): { layerX: number; layerY: number } { 29 | const realEvent: any = e.nativeEvent; 30 | const rect = canvasRef.current.getBoundingClientRect(); 31 | const x = realEvent.clientX - rect.left; 32 | const y = realEvent.clientY - rect.top; 33 | return { layerX: x, layerY: y }; 34 | } 35 | 36 | function onPointerDown(e: any): void { 37 | const { layerX, layerY } = getlayerXY(e); 38 | const fanClickPos = FanPosition.createFanPosByCanPos( 39 | layerX, 40 | layerY, 41 | canvas.width, 42 | canvas.height 43 | ); 44 | pointerDownPos.current = [layerX, layerY]; 45 | pointerDownTime.current = Date.parse(new Date().toString()); 46 | canvas.onPointerDown?.call(canvas, fanClickPos); 47 | onDragDown(e); 48 | } 49 | 50 | function onPointerUp(e: any): void { 51 | const { layerX, layerY } = getlayerXY(e); 52 | const fanClickPos = FanPosition.createFanPosByCanPos( 53 | layerX, 54 | layerY, 55 | canvas.width, 56 | canvas.height 57 | ); 58 | pointerUpPos.current = [layerX, layerY]; 59 | pointerUpTime.current = Date.parse(new Date().toString()); 60 | canvas.onPointerUp?.call(canvas, fanClickPos); 61 | //call PointPressEvent 62 | if ( 63 | approximatelyEqual( 64 | pointerDownPos.current[0], 65 | pointerUpPos.current[0], 66 | 3 67 | ) && 68 | approximatelyEqual(pointerDownPos.current[1], pointerUpPos.current[1], 3) 69 | ) { 70 | if (pointerUpTime.current - pointerDownTime.current <= 1000) 71 | onPointerShortPress(e); 72 | else onPointLongPress(e); 73 | } 74 | //console.log(`pressDownTime=${pointerDownTime.current} pressUpTime=${pointerUpTime.current}`) 75 | if (pointerIsDrag.current) { 76 | pointerIsDrag.current = false; 77 | } 78 | } 79 | 80 | function onPointerMove(e: any): void { 81 | const { layerX, layerY } = getlayerXY(e); 82 | const fanClickPos = FanPosition.createFanPosByCanPos( 83 | layerX, 84 | layerY, 85 | canvas.width, 86 | canvas.height 87 | ); 88 | canvas.onPointerMove?.call(canvas, fanClickPos); 89 | if (pointerIsDrag.current) { 90 | onDraging(e); 91 | } 92 | } 93 | function onPointerLeave(_e: any): void { 94 | if (pointerIsDrag.current) { 95 | pointerIsDrag.current = false; 96 | } 97 | } 98 | 99 | function onPointerShortPress(e: any): void { 100 | const { layerX, layerY } = getlayerXY(e); 101 | const fanClickPos = FanPosition.createFanPosByCanPos( 102 | layerX, 103 | layerY, 104 | canvas.width, 105 | canvas.height 106 | ); 107 | canvas.onPointerShortPress?.call(canvas, fanClickPos); 108 | } 109 | //@ts-ignore 110 | function onPointLongPress(e: any): void {} 111 | function onDragDown(e: any): void { 112 | const { layerX, layerY } = getlayerXY(e); 113 | const fanClickPos = FanPosition.createFanPosByCanPos( 114 | layerX, 115 | layerY, 116 | canvas.width, 117 | canvas.height 118 | ); 119 | pointerIsDrag.current = canvas.onPointerDragDown?.call( 120 | canvas, 121 | fanClickPos 122 | )!!; 123 | } 124 | 125 | function onDraging(e: any): void { 126 | const { layerX, layerY } = getlayerXY(e); 127 | const fanClickPos = FanPosition.createFanPosByCanPos( 128 | layerX, 129 | layerY, 130 | canvas.width, 131 | canvas.height 132 | ); 133 | canvas.onPointerDraging?.call(canvas, fanClickPos); 134 | } 135 | 136 | const { ...option } = canvas; 137 | 138 | return ( 139 | onClickCanvas(e)} 142 | onPointerDown={(e: any) => { 143 | onPointerDown(e); 144 | }} 145 | onPointerMove={(e: any) => { 146 | onPointerMove(e); 147 | }} 148 | onPointerUp={(e: any) => { 149 | onPointerUp(e); 150 | }} 151 | onPointerLeave={(e: any) => { 152 | onPointerLeave(e); 153 | }} 154 | {...option} 155 | /> 156 | ); 157 | }; 158 | 159 | const approximatelyEqual = (a: number, b: number, error: number) => { 160 | return Math.abs(b - a) <= error; 161 | }; 162 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./SlowSliderField" 2 | export * from "./gpu" 3 | export * from "./cpu" 4 | export * from "./settings" 5 | export * from "./fan" 6 | export * from "./fanCanvas" 7 | export * from "./more" 8 | export * from "./customTDP" 9 | export * from "./power" 10 | export * from "./actionButtonItem" 11 | 12 | -------------------------------------------------------------------------------- /src/components/more.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ButtonItem, 3 | Field, 4 | PanelSection, 5 | PanelSectionRow, 6 | ToggleField, 7 | } from "@decky/ui"; 8 | import { FC, useEffect, useState } from "react"; 9 | import { localizationManager, localizeStrEnum } from "../i18n"; 10 | import { Backend, Settings, compareVersions } from "../util"; 11 | import { ActionButtonItem } from "."; 12 | 13 | export const MoreComponent: FC<{ isTab?: boolean }> = ({ isTab = false }) => { 14 | const [currentVersion, setCurrentVersion] = useState( 15 | Backend.data?.getCurrentVersion() || "" 16 | ); 17 | const [latestVersion, setLatestVersion] = useState( 18 | Backend.data?.getLatestVersion() || "" 19 | ); 20 | 21 | const [useOldUI, setUseOldUI] = useState( 22 | Settings.useOldUI 23 | ); 24 | 25 | const updateUseOldUI = (value: boolean) => { 26 | Settings.useOldUI = value; 27 | setUseOldUI(value); 28 | }; 29 | 30 | useEffect(() => { 31 | const getData = async () => { 32 | setTimeout(() => { 33 | setLatestVersion(latestVersion); 34 | Backend.getLatestVersion().then((latestVersion) => { 35 | setLatestVersion(latestVersion); 36 | }); 37 | Backend.getCurrentVersion().then((currentVersion) => { 38 | setCurrentVersion(currentVersion); 39 | }); 40 | }, 3000); 41 | }; 42 | getData(); 43 | }); 44 | 45 | let uptButtonText = 46 | localizationManager.getString(localizeStrEnum.REINSTALL_PLUGIN) || 47 | "Reinstall Plugin"; 48 | 49 | if (currentVersion !== latestVersion && Boolean(latestVersion)) { 50 | const versionCompare = compareVersions(latestVersion, currentVersion); 51 | if (versionCompare > 0) { 52 | uptButtonText = `${ 53 | localizationManager.getString(localizeStrEnum.UPDATE_PLUGIN) || "Update" 54 | } ${latestVersion}`; 55 | } else if (versionCompare < 0) { 56 | uptButtonText = `${ 57 | localizationManager.getString(localizeStrEnum.ROLLBACK_PLUGIN) || 58 | "Rollback" 59 | } ${latestVersion}`; 60 | } 61 | } 62 | 63 | return ( 64 |
65 | 68 | 69 | { 76 | updateUseOldUI(value); 77 | }} 78 | /> 79 | 80 | 81 | { 84 | await Backend.updateLatest(); 85 | }} 86 | > 87 | {uptButtonText} 88 | 89 | 90 | 91 | { 94 | Settings.resetToLocalStorage(); 95 | }} 96 | > 97 | {localizationManager.getString(localizeStrEnum.RESET_ALL) || 98 | "Reset All"} 99 | 100 | 101 | 102 | 111 | {currentVersion} 112 | 113 | 114 | {Boolean(latestVersion) && ( 115 | 116 | 124 | {latestVersion} 125 | 126 | 127 | )} 128 | 129 |
130 | ); 131 | }; 132 | -------------------------------------------------------------------------------- /src/components/power.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ButtonItem, 3 | PanelSection, 4 | PanelSectionRow, 5 | ToggleField, 6 | } from "@decky/ui"; 7 | import { FC, useEffect, useMemo, useState } from "react"; 8 | import { 9 | Backend, 10 | ComponentName, 11 | PluginManager, 12 | Settings, 13 | UpdateType, 14 | } from "../util"; 15 | import { SlowSliderField } from "./SlowSliderField"; 16 | import { localizationManager, localizeStrEnum } from "../i18n"; 17 | import { RiArrowDownSFill, RiArrowUpSFill } from "react-icons/ri"; 18 | 19 | const BypassChargeComponent: FC = () => { 20 | const [bypassCharge, setBypassCharge] = useState( 21 | Settings.appBypassCharge() 22 | ); 23 | 24 | // const [chargeLimit, setChargeLimit] = useState( 25 | // Settings.appChargeLimit() 26 | // ); 27 | 28 | const refresh = () => { 29 | setBypassCharge(Settings.appBypassCharge()); 30 | // setChargeLimit(Settings.appChargeLimit()); 31 | }; 32 | 33 | useEffect(() => { 34 | PluginManager.listenUpdateComponent( 35 | ComponentName.POWER_BYPASS_CHARGE, 36 | [ 37 | ComponentName.POWER_BYPASS_CHARGE, 38 | ComponentName.POWER_ALL, 39 | ComponentName.POWER_CHARGE_LIMIT, 40 | ], 41 | (_ComponentName, updateType) => { 42 | switch (updateType) { 43 | case UpdateType.UPDATE: { 44 | refresh(); 45 | break; 46 | } 47 | } 48 | } 49 | ); 50 | }, []); 51 | 52 | // useEffect(() => { 53 | // // 实时获取旁路供电状态 54 | // Backend.getBypassCharge().then((value) => { 55 | // setBypassCharge(value); 56 | // }); 57 | // }, [bypassCharge]); 58 | 59 | return ( 60 | <> 61 | 62 | { 71 | Settings.setBypassCharge(value); 72 | }} 73 | /> 74 | 75 | 76 | ); 77 | }; 78 | 79 | const ChargeLimitComponent: FC = () => { 80 | const [chargeLimit, setChargeLimit] = useState( 81 | Settings.appChargeLimit() 82 | ); 83 | const [bypassCharge, setBypassCharge] = useState( 84 | Settings.appBypassCharge() 85 | ); 86 | 87 | const [supportsResetChargeLimit, __] = useState( 88 | Backend.data.isSupportResetChargeLimit() 89 | ); 90 | 91 | const [enableChargeLimit, setEnableChargeLimit] = useState( 92 | Settings.appEnableChargeLimit() 93 | ); 94 | 95 | const refresh = () => { 96 | setChargeLimit(Settings.appChargeLimit()); 97 | setBypassCharge(Settings.appBypassCharge()); 98 | setEnableChargeLimit(Settings.appEnableChargeLimit()); 99 | }; 100 | 101 | useEffect(() => { 102 | PluginManager.listenUpdateComponent( 103 | ComponentName.POWER_CHARGE_LIMIT, 104 | [ 105 | ComponentName.POWER_CHARGE_LIMIT, 106 | ComponentName.POWER_ALL, 107 | ComponentName.POWER_BYPASS_CHARGE, 108 | ], 109 | (_ComponentName, updateType) => { 110 | switch (updateType) { 111 | case UpdateType.UPDATE: { 112 | refresh(); 113 | break; 114 | } 115 | } 116 | } 117 | ); 118 | }, []); 119 | 120 | return ( 121 | <> 122 | {supportsResetChargeLimit && ( 123 | 124 | { 128 | Settings.setEnableChargeLimit(enableChargeLimit); 129 | }} 130 | /> 131 | 132 | )} 133 | {((supportsResetChargeLimit && enableChargeLimit) || 134 | !supportsResetChargeLimit) && ( 135 | 136 | { 160 | Settings.setChargeLimit(value); 161 | }} 162 | /> 163 | 164 | )} 165 | 166 | ); 167 | }; 168 | 169 | export const PowerComponent: FC<{ isTab?: boolean }> = ({ isTab = false }) => { 170 | const [show, setShow] = useState(Settings.ensureEnable()); 171 | 172 | const supportChargeLimit = useMemo(() => { 173 | return Backend.data.getIsSupportChargeLimit(); 174 | }, []); 175 | 176 | const isSupportSoftwareChargeLimit = useMemo(() => { 177 | return Backend.data.isSupportSoftwareChargeLimit(); 178 | }, []); 179 | 180 | const [showPowerMenu, setShowPowerMenu] = useState( 181 | Settings.showPowerMenu 182 | ); 183 | const updateShowPowerMenu = (show: boolean) => { 184 | setShowPowerMenu(show); 185 | Settings.showPowerMenu = show; 186 | }; 187 | 188 | const hide = (ishide: boolean) => { 189 | setShow(!ishide); 190 | }; 191 | 192 | useEffect(() => { 193 | PluginManager.listenUpdateComponent( 194 | ComponentName.POWER_ALL, 195 | [ComponentName.POWER_ALL], 196 | (_ComponentName, updateType) => { 197 | switch (updateType) { 198 | case UpdateType.HIDE: { 199 | hide(true); 200 | break; 201 | } 202 | case UpdateType.SHOW: { 203 | hide(false); 204 | break; 205 | } 206 | } 207 | } 208 | ); 209 | }, []); 210 | 211 | useEffect(() => { 212 | setShow( 213 | Settings.ensureEnable() && 214 | (supportChargeLimit || isSupportSoftwareChargeLimit) 215 | ); 216 | }, [supportChargeLimit, isSupportSoftwareChargeLimit]); 217 | 218 | return ( 219 |
220 | {show && ( 221 | 222 | {!isTab && ( 223 | 224 | updateShowPowerMenu(!showPowerMenu)} 234 | > 235 | {showPowerMenu ? : } 236 | 237 | 238 | )} 239 | {(showPowerMenu || isTab) && ( 240 | <> 241 | {supportChargeLimit && } 242 | {isSupportSoftwareChargeLimit && } 243 | 244 | )} 245 | 246 | )} 247 |
248 | ); 249 | }; 250 | -------------------------------------------------------------------------------- /src/i18n/bulgarian.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"Настройки", 3 | "ENABLE_SETTINGS":"Активиране на настройките", 4 | "USE_PERGAME_PROFILE":"Използване на профил за игра", 5 | "USING":"Използва се", 6 | "DEFAULT":"По подразбиране", 7 | "PROFILE":"Профил", 8 | "CPU_BOOST":"CPU Boost", 9 | "CPU_BOOST_DESC":"Увеличаване на максималната честота на CPU", 10 | "HT_DESC":"Активиране на Intel Hyper-Threading", 11 | "SMT_DESC":"Активиране на едновременна многонишковост", 12 | "CPU_NUM":"Брой CPU ядра", 13 | "CPU_NUM_DESC":"Задаване на активните физически ядра", 14 | "CPU_MAX_PERF":"Максимална производителност на CPU", 15 | "CPU_MAX_PERF_AUTO":"Автоматична максимална производителност на CPU", 16 | "CPU_GOVERNOR": "Регулатор на CPU", 17 | "CPU_GOVERNOR_DESC": "Задаване на политика за планиране на производителността на CPU", 18 | "TDP":"Лимит на термична мощност (TDP)", 19 | "TDP_DESC":"Ограничаване на мощността на процесора за по-ниска обща консумация", 20 | "RYZENADJ_NOT_FOUND":"RyzenAdj не е намерен", 21 | "WATTS":"Вата", 22 | "GPU_FREQMODE":"Режим на честота на GPU", 23 | "UNLIMITED":"Неограничен", 24 | "FIXED_FREQ":"Фиксиран", 25 | "RANGE_FREQ":"Диапазон", 26 | "AUTO_FREQ":"Автоматичен", 27 | "GPU_FIX_FREQ":"Честота на GPU", 28 | "GPU_MIN_FREQ":"Минимална честота", 29 | "GPU_MAX_FREQ":"Максимална честота", 30 | "FAN_SPEED":"Скорост на вентилатора", 31 | "CREATE_FAN_PROFILE":"Създаване на профил за вентилатора", 32 | "GRID_ALIG":"Подравняване по мрежа", 33 | "FAN_MODE":"Режим на вентилатора", 34 | "NOT_CONTROLLED":"Без контрол", 35 | "FIXED":"Фиксиран", 36 | "CURVE":"Крива", 37 | "SNAP_GRIDLINE":"Прилепване към пресечните точки на мрежата", 38 | "FAN_SPEED_PERCENT":"Скорост на вентилатора (%)", 39 | "SENSOR_TEMP":"Температура на сензора", 40 | "CREATE_FAN_PROFILE_TIP":"Създаване на профил за вентилатора", 41 | "SELECT_FAN_PROFILE_TIP":"Избор на профил за вентилатора", 42 | "FAN_PROFILE_NAME":"Име на профила", 43 | "USE":"Използване", 44 | "DELETE":"Изтриване", 45 | "CREATE":"Създаване", 46 | "CANCEL":"Отказ", 47 | "CURENT_STAT":"Текущо състояние", 48 | "EDIT":"Редактиране", 49 | "SAVE":"Запазване", 50 | "NATIVE_FREQ":"Нативен", 51 | "MORE":"Още", 52 | "REINSTALL_PLUGIN": "Преинсталиране на плъгина", 53 | "UPDATE_PLUGIN": "Обновяване до", 54 | "ROLLBACK_PLUGIN": "Връщане до", 55 | "INSTALLED_VERSION": "Инсталирана версия", 56 | "LATEST_VERSION": "Последна версия", 57 | "GPU_NATIVE_SLIDER": "Нативен GPU плъзгач", 58 | "GPU_NATIVE_SLIDER_DESC": "Активиране на нативния плъзгач за контрол на GPU", 59 | "USE_PERACMODE_PROFILE": "Използване на профил според захранването", 60 | "AC_MODE": "Режим на захранване", 61 | "BAT_MODE": "Режим на батерия", 62 | "CUSTOM_TDP_RANGE": "Персонализиран диапазон на TDP плъзгача", 63 | "RESET_ALL": "Нулиране на всичко", 64 | "NATIVE_FREQ_DESC": "Задаване на честота чрез системното меню", 65 | "UNLIMITED_DESC": "Без ограничение на GPU честотата, системно планиране по подразбиране", 66 | "FIXED_FREQ_DESC": "Фиксирана GPU честота", 67 | "RANGE_FREQ_DESC": "Задаване на диапазон на GPU честотата", 68 | "AUTO_FREQ_DESC": "Адаптивна GPU честота, принудително изключен TDP лимит, изключен Boost", 69 | "AUTO_FREQ_TDP_NOTIF": "GPU режим {{mode}}, TDP лимитът е изключен", 70 | "NATIVE_TDP_SLIDER": "Нативен TDP плъзгач", 71 | "NATIVE_TDP_SLIDER_DESC": "Активирайте нативния плъзгач TDP, изисква се рестартиране на системата, за да влезе в сила активирането/деактивирането. Промяната на максималната стойност на диапазона TDP изисква рестартиране на Steam, за да влезе в сила. След активиране настройките TDP се запазват от Steam, а настройките за състояние на захранването в плъгина няма да обработват конфигурацията TDP", 72 | "FORCE_SHOW_TDP": "Принудително показване на TDP контрол", 73 | "FORCE_SHOW_TDP_DESC": "По подразбиране плъгинът обработва нативния TDP плъзгач. Ако нативният плъзгач има проблеми, активирайте тази опция, за да използвате вътрешния TDP контрол на плъгина", 74 | "MANUAL_BYPASS_CHARGE": "Ръчно байпас зареждане", 75 | "MANUAL_BYPASS_CHARGE_DESC": "Ръчно управление на байпас зареждането", 76 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "Активирането ще деактивира временно ограничението за зареждане", 77 | "CHARGE_LIMIT": "Ограничение за зареждане", 78 | "CHARGE_LIMIT_DESC": "Задаване на ограничение за зареждане за удължаване на живота на батерията", 79 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "Байпас зареждането е активно, ограничението за зареждане е временно деактивирано", 80 | "USE_OLD_UI": "Използвайте стария интерфейс", 81 | "USE_OLD_UI_DESC": "Използвайте стария изглед със списък вместо новия изглед с раздели" 82 | } -------------------------------------------------------------------------------- /src/i18n/english.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"Settings", 3 | "ENABLE_SETTINGS":"Enable Settings", 4 | "USE_PERGAME_PROFILE":"Use per-game Profile", 5 | "USING":"Using", 6 | "DEFAULT":" default ", 7 | "PROFILE":"Profile", 8 | "CPU_BOOST":"CPU Boost", 9 | "CPU_BOOST_DESC":"Increase the maximum CPU frequency", 10 | "HT_DESC":"Enable Hyper-threading", 11 | "SMT_DESC":"Enable Simultaneous Multi-Threading", 12 | "CPU_NUM":"Number Of CPU Cores", 13 | "CPU_NUM_DESC":"Set the enabled physical core", 14 | "CPU_MAX_PERF":"CPU Maximum Performance", 15 | "CPU_MAX_PERF_AUTO":"Auto CPU Maximum Performance", 16 | "CPU_GOVERNOR": "CPU Governor", 17 | "CPU_GOVERNOR_DESC": "Select CPU frequency scaling governor", 18 | "CPU_EPP": "Energy Performance Preference", 19 | "CPU_EPP_DESC": "Balance between energy efficiency and performance", 20 | "TDP":"Thermal Power (TDP) Limit", 21 | "TDP_DESC":"Limits processor power for less total power", 22 | "RYZENADJ_NOT_FOUND":"RyzenAdj Not Detected", 23 | "WATTS":"Watts", 24 | "GPU_FREQMODE":"GPU Clock Frequency Mode", 25 | "UNLIMITED":"Unlimited", 26 | "FIXED_FREQ":"Fixed", 27 | "RANGE_FREQ":"Range", 28 | "AUTO_FREQ":"Auto", 29 | "GPU_FIX_FREQ":"GPU Clock Frequency", 30 | "GPU_MIN_FREQ":"Minimum Frequency Limit", 31 | "GPU_MAX_FREQ":"Maximum Frequency Limit", 32 | "FAN_SPEED":"Fan Speed", 33 | "CREATE_FAN_PROFILE":"Create Fan Profile", 34 | "GRID_ALIG":"Grid Alignment", 35 | "FAN_MODE":"Fan Mode", 36 | "NOT_CONTROLLED":"Not Controlled", 37 | "FIXED":"Fixed", 38 | "CURVE":"Curve", 39 | "SNAP_GRIDLINE":"Snap To The Gridline Intersection", 40 | "FAN_SPEED_PERCENT":"Fan Speed Percentage", 41 | "SENSOR_TEMP":"Sensor Temperature", 42 | "CREATE_FAN_PROFILE_TIP":"Create a Fan Profile", 43 | "SELECT_FAN_PROFILE_TIP":"Select a Fan Profile", 44 | "FAN_PROFILE_NAME":"Profile Name", 45 | "USE":"Use", 46 | "DELETE":"Delete", 47 | "CREATE":"Create", 48 | "CANCEL":"Cancel", 49 | "CURENT_STAT":"Current status", 50 | "EDIT":"Edit", 51 | "SAVE":"Save", 52 | "NATIVE_FREQ":"Native", 53 | "MORE":"More", 54 | "REINSTALL_PLUGIN": "Reinstall Plugin", 55 | "UPDATE_PLUGIN": "Update to", 56 | "ROLLBACK_PLUGIN": "Rollback to", 57 | "INSTALLED_VERSION": "Installed Version", 58 | "LATEST_VERSION": "Latest Version", 59 | "GPU_NATIVE_SLIDER": "Native GPU Slider", 60 | "GPU_NATIVE_SLIDER_DESC": "Enable the native GPU control slider", 61 | "USE_PERACMODE_PROFILE": "Use per-AC Mode Profile", 62 | "AC_MODE": "AC Mode", 63 | "BAT_MODE": "Battery Mode", 64 | "CUSTOM_TDP_RANGE": "Custom TDP Slider Range", 65 | "RESET_ALL": "Reset All", 66 | "NATIVE_FREQ_DESC": "Set frequency using system shortcut menu", 67 | "UNLIMITED_DESC": "No limit on GPU frequency, system default scheduling", 68 | "FIXED_FREQ_DESC": "Fixed GPU frequency", 69 | "RANGE_FREQ_DESC": "Set GPU frequency range", 70 | "AUTO_FREQ_DESC": "Adaptive GPU frequency, forcibly disabled TDP limit, disabled Boost", 71 | "AUTO_FREQ_TDP_NOTIF": "GPU mode {{mode}}, TDP limit disabled", 72 | "NATIVE_TDP_SLIDER": "Native TDP Slider", 73 | "NATIVE_TDP_SLIDER_DESC": "Enable native TDP slider, requires system restart to take effect when enabling/disabling. Modifying TDP range maximum requires Steam restart to take effect. After enabling, TDP settings are saved by Steam, and power state settings in plugin will not handle TDP configuration", 74 | "FORCE_SHOW_TDP": "Force Show TDP Control", 75 | "FORCE_SHOW_TDP_DESC": "By default, the plugin will process the native TDP slider. If there is an issue with the native slider, enable this option to use the plugin's internal TDP control", 76 | "MANUAL_BYPASS_CHARGE": "Manual Bypass Charging", 77 | "MANUAL_BYPASS_CHARGE_DESC": "Manually control bypass charging", 78 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "Enabling this will temporarily disable charge limit", 79 | "CHARGE_LIMIT": "Charge Limit", 80 | "CHARGE_LIMIT_DESC": "Set battery charge limit to extend battery life", 81 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "Manual bypass charging is enabled, charge limit is temporarily disabled", 82 | "USE_OLD_UI": "Use Old UI", 83 | "USE_OLD_UI_DESC": "Use the old List View instead of the new Tab View" 84 | } -------------------------------------------------------------------------------- /src/i18n/french.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"Paramètres", 3 | "ENABLE_SETTINGS":"Activer les paramètres", 4 | "USE_PERGAME_PROFILE":"Utiliser un profil par jeu", 5 | "USING":"En cours d'utilisation", 6 | "DEFAULT":"Par défaut", 7 | "PROFILE":"Profil", 8 | "CPU_BOOST":"Boost CPU", 9 | "CPU_BOOST_DESC":"Augmenter la fréquence maximale du processeur", 10 | "HT_DESC":"Activer l'Hyper-Threading Intel", 11 | "SMT_DESC":"Activer le multithreading simultané", 12 | "CPU_NUM":"Nombre de cœurs processeur", 13 | "CPU_NUM_DESC":"Définir les cœurs physiques activés", 14 | "CPU_MAX_PERF":"Performance maximale du CPU", 15 | "CPU_MAX_PERF_AUTO":"Auto CPU Maximum Performance", 16 | "CPU_GOVERNOR": "Gouverneur CPU", 17 | "CPU_GOVERNOR_DESC": "Définir la politique de planification des performances du CPU", 18 | "TDP":"Limite de puissance thermique (TDP)", 19 | "TDP_DESC":"Limite la puissance du processeur pour réduire la consommation totale", 20 | "RYZENADJ_NOT_FOUND":"RyzenAdj non trouvé", 21 | "WATTS":"Watts", 22 | "GPU_FREQMODE":"Mode de fréquence GPU", 23 | "UNLIMITED":"Illimité", 24 | "FIXED_FREQ":"Fixe", 25 | "RANGE_FREQ":"Plage", 26 | "AUTO_FREQ":"Auto", 27 | "GPU_FIX_FREQ":"Fréquence GPU", 28 | "GPU_MIN_FREQ":"Fréquence minimale", 29 | "GPU_MAX_FREQ":"Fréquence maximale", 30 | "FAN_SPEED":"Vitesse du ventilateur", 31 | "CREATE_FAN_PROFILE":"Créer un profil de ventilateur", 32 | "GRID_ALIG":"Alignement de la grille", 33 | "FAN_MODE":"Mode ventilateur", 34 | "NOT_CONTROLLED":"Non contrôlé", 35 | "FIXED":"Fixe", 36 | "CURVE":"Courbe", 37 | "SNAP_GRIDLINE":"Aligner sur les intersections de la grille", 38 | "FAN_SPEED_PERCENT":"Vitesse du ventilateur (%)", 39 | "SENSOR_TEMP":"Température du capteur", 40 | "CREATE_FAN_PROFILE_TIP":"Créer un profil de ventilateur", 41 | "SELECT_FAN_PROFILE_TIP":"Sélectionner un profil de ventilateur", 42 | "FAN_PROFILE_NAME":"Nom du profil", 43 | "USE":"Utiliser", 44 | "DELETE":"Supprimer", 45 | "CREATE":"Créer", 46 | "CANCEL":"Annuler", 47 | "CURENT_STAT":"État actuel", 48 | "EDIT":"Modifier", 49 | "SAVE":"Enregistrer", 50 | "NATIVE_FREQ":"Natif", 51 | "MORE":"Plus", 52 | "REINSTALL_PLUGIN": "Réinstaller le plugin", 53 | "UPDATE_PLUGIN": "Mettre à jour vers", 54 | "ROLLBACK_PLUGIN": "Revenir à", 55 | "INSTALLED_VERSION": "Version installée", 56 | "LATEST_VERSION": "Dernière version", 57 | "GPU_NATIVE_SLIDER": "Curseur GPU natif", 58 | "GPU_NATIVE_SLIDER_DESC": "Activer le curseur de contrôle GPU natif", 59 | "USE_PERACMODE_PROFILE": "Utiliser un profil par mode d'alimentation", 60 | "AC_MODE": "Mode secteur", 61 | "BAT_MODE": "Mode batterie", 62 | "CUSTOM_TDP_RANGE": "Plage TDP personnalisée", 63 | "RESET_ALL": "Tout réinitialiser", 64 | "NATIVE_FREQ_DESC": "Régler la fréquence via le menu système", 65 | "UNLIMITED_DESC": "Pas de limite de fréquence GPU, planification système par défaut", 66 | "FIXED_FREQ_DESC": "Fréquence GPU fixe", 67 | "RANGE_FREQ_DESC": "Définir la plage de fréquence GPU", 68 | "AUTO_FREQ_DESC": "Fréquence GPU adaptative, limite TDP désactivée de force, Boost désactivé", 69 | "AUTO_FREQ_TDP_NOTIF": "Mode GPU {{mode}}, limite TDP désactivée", 70 | "NATIVE_TDP_SLIDER": "Curseur TDP natif", 71 | "NATIVE_TDP_SLIDER_DESC": "Activer le curseur TDP natif, nécessite un redémarrage du système pour que l'activation/désactivation prenne effet. La modification de la valeur maximale de la plage TDP nécessite un redémarrage de Steam pour prendre effet. Après activation, les paramètres TDP sont sauvegardés par Steam, et les paramètres d'état d'alimentation dans le plugin ne géreront pas la configuration TDP", 72 | "FORCE_SHOW_TDP": "Forcer l'affichage du contrôle TDP", 73 | "FORCE_SHOW_TDP_DESC": "Par défaut, le plugin traite le curseur TDP natif. Si le curseur natif pose problème, activez cette option pour utiliser le contrôle TDP interne du plugin", 74 | "MANUAL_BYPASS_CHARGE": "Charge en dérivation manuelle", 75 | "MANUAL_BYPASS_CHARGE_DESC": "Contrôler manuellement la charge en dérivation", 76 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "L'activation désactivera temporairement la limite de charge", 77 | "CHARGE_LIMIT": "Limite de charge", 78 | "CHARGE_LIMIT_DESC": "Définir une limite de charge pour prolonger la durée de vie de la batterie", 79 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "Charge en dérivation activée, limite de charge temporairement désactivée", 80 | "USE_OLD_UI": "Utiliser l'ancienne interface", 81 | "USE_OLD_UI_DESC": "Utiliser l'ancienne vue en liste au lieu de la nouvelle vue par onglets" 82 | } -------------------------------------------------------------------------------- /src/i18n/german.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"Einstellungen", 3 | "ENABLE_SETTINGS":"Einstellungen aktivieren", 4 | "USE_PERGAME_PROFILE":"Spielbezogene Profil", 5 | "USING":"Verwendet", 6 | "DEFAULT":"Standard", 7 | "PROFILE":"Profil", 8 | "CPU_BOOST":"CPU Boost", 9 | "CPU_BOOST_DESC":"Maximale CPU Frequenz erhöhen", 10 | "HT_DESC":"Hyperthreading aktivieren", 11 | "SMT_DESC":"Simultanes Multithreading aktivieren", 12 | "CPU_NUM":"Anzahl CPU-Kerne", 13 | "CPU_NUM_DESC":"Anzahl physischer CPU-Kerne", 14 | "CPU_MAX_PERF":"CPU Maximale Leistung", 15 | "CPU_MAX_PERF_AUTO":"Automatische CPU Maximale Leistung", 16 | "CPU_GOVERNOR": "CPU-Taktgeber", 17 | "CPU_GOVERNOR_DESC": "CPU-Leistungsplanungsrichtlinie festlegen", 18 | "TDP":"Thermische Verlustleistung (TDP) Limit", 19 | "TDP_DESC":"CPU-Leistung einschränken um Strom zu sparen", 20 | "RYZENADJ_NOT_FOUND":"RyzenAdj nicht gefunden", 21 | "WATTS":"Watt", 22 | "GPU_FREQMODE":"GPU-Frequenzmodus", 23 | "UNLIMITED":"Unbeschränkt", 24 | "FIXED_FREQ":"Fest", 25 | "RANGE_FREQ":"Bereich", 26 | "AUTO_FREQ":"Automatisch", 27 | "GPU_FIX_FREQ":"GPU-Frequenz", 28 | "GPU_MIN_FREQ":"Minimale Frequenz", 29 | "GPU_MAX_FREQ":"Maximale Frequenz", 30 | "FAN_SPEED":"Lüftergeschwindigkeit", 31 | "CREATE_FAN_PROFILE":"Lüfterprofil erstellen", 32 | "GRID_ALIG":"Gitternetz-Ausrichtung", 33 | "FAN_MODE":"Lüftermodus", 34 | "NOT_CONTROLLED":"Nicht gesteuert", 35 | "FIXED":"Fest", 36 | "CURVE":"Kurve", 37 | "SNAP_GRIDLINE":"An Gitterlinien ausrichten", 38 | "FAN_SPEED_PERCENT":"Lüftergeschwindigkeit (%)", 39 | "SENSOR_TEMP":"Sensortemperatur", 40 | "CREATE_FAN_PROFILE_TIP":"Lüfterprofil erstellen", 41 | "SELECT_FAN_PROFILE_TIP":"Lüfterprofil auswählen", 42 | "FAN_PROFILE_NAME":"Profilname", 43 | "USE":"Verwenden", 44 | "DELETE":"Löschen", 45 | "CREATE":"Erstellen", 46 | "CANCEL":"Abbrechen", 47 | "CURENT_STAT":"Aktueller Status", 48 | "EDIT":"Bearbeiten", 49 | "SAVE":"Speichern", 50 | "NATIVE_FREQ":"Nativ", 51 | "MORE":"Mehr", 52 | "REINSTALL_PLUGIN": "Plugin neu installieren", 53 | "UPDATE_PLUGIN": "Aktualisieren auf", 54 | "ROLLBACK_PLUGIN": "Zurücksetzen auf", 55 | "INSTALLED_VERSION": "Installierte Version", 56 | "LATEST_VERSION": "Neueste Version", 57 | "GPU_NATIVE_SLIDER": "Nativer GPU-Schieberegler", 58 | "GPU_NATIVE_SLIDER_DESC": "Nativen GPU-Schieberegler aktivieren", 59 | "USE_PERACMODE_PROFILE": "Profil nach Stromversorgung", 60 | "AC_MODE": "Netzbetrieb", 61 | "BAT_MODE": "Akkubetrieb", 62 | "CUSTOM_TDP_RANGE": "Benutzerdefinierter TDP-Bereich", 63 | "RESET_ALL": "Alles zurücksetzen", 64 | "NATIVE_FREQ_DESC": "Frequenz über Systemmenü einstellen", 65 | "UNLIMITED_DESC": "Keine GPU-Frequenzbegrenzung, Standard-Systemplanung", 66 | "FIXED_FREQ_DESC": "Feste GPU-Frequenz", 67 | "RANGE_FREQ_DESC": "GPU-Frequenzbereich festlegen", 68 | "AUTO_FREQ_DESC": "Adaptive GPU-Frequenz, TDP-Limit deaktiviert, Boost deaktiviert", 69 | "AUTO_FREQ_TDP_NOTIF": "GPU-Modus {{mode}}, TDP-Limit deaktiviert", 70 | "NATIVE_TDP_SLIDER": "Nativer TDP-Schieberegler", 71 | "NATIVE_TDP_SLIDER_DESC": "Aktivieren Sie den nativen TDP-Schieberegler, ein Neustart des Systems ist erforderlich, um das Aktivieren/Deaktivieren wirksam zu machen. Die Änderung des TDP-Bereichsmaximums erfordert einen Steam-Neustart, um wirksam zu werden. Nach der Aktivierung werden die TDP-Einstellungen von Steam gespeichert, und die Stromzustandseinstellungen im Plugin werden die TDP-Konfiguration nicht verarbeiten", 72 | "FORCE_SHOW_TDP": "TDP-Steuerung erzwingen", 73 | "FORCE_SHOW_TDP_DESC": "Standardmäßig verarbeitet das Plugin den nativen TDP-Schieberegler. Bei Problemen mit dem nativen Schieberegler aktivieren Sie diese Option, um die interne TDP-Steuerung des Plugins zu verwenden", 74 | "MANUAL_BYPASS_CHARGE": "Manuelle Bypass-Ladung", 75 | "MANUAL_BYPASS_CHARGE_DESC": "Bypass-Ladung manuell steuern", 76 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "Bei Aktivierung wird die Ladelimitierung vorübergehend deaktiviert", 77 | "CHARGE_LIMIT": "Ladelimitierung", 78 | "CHARGE_LIMIT_DESC": "Ladelimitierung zum Schutz der Akkulebensdauer einstellen", 79 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "Bypass-Ladung ist aktiv, Ladelimitierung vorübergehend deaktiviert", 80 | "USE_OLD_UI": "Alte Benutzeroberfläche verwenden", 81 | "USE_OLD_UI_DESC": "Alte Listenansicht anstelle der neuen Tab-Ansicht verwenden" 82 | } -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./localization" 2 | export * from "./localizeMap" -------------------------------------------------------------------------------- /src/i18n/italian.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"Impostazioni", 3 | "ENABLE_SETTINGS":"Abilita impostazioni", 4 | "USE_PERGAME_PROFILE":"Usa profilo per gioco", 5 | "USING":"In uso", 6 | "DEFAULT":"Predefinito", 7 | "PROFILE":"Profilo", 8 | "CPU_BOOST":"Boost CPU", 9 | "CPU_BOOST_DESC":"Aumenta la frequenza massima della CPU", 10 | "HT_DESC":"Abilita Intel Hyper-Threading", 11 | "SMT_DESC":"Abilita il multithreading simultaneo", 12 | "CPU_NUM":"Numero di core CPU", 13 | "CPU_NUM_DESC":"Imposta i core fisici attivi", 14 | "CPU_MAX_PERF":"Prestazioni massime della CPU", 15 | "CPU_MAX_PERF_AUTO":"Auto prestazioni massime della CPU", 16 | "CPU_GOVERNOR": "Governatore CPU", 17 | "CPU_GOVERNOR_DESC": "Imposta la politica di pianificazione delle prestazioni della CPU", 18 | "TDP":"Limite di potenza termica (TDP)", 19 | "TDP_DESC":"Limita la potenza del processore per ridurre il consumo totale", 20 | "RYZENADJ_NOT_FOUND":"RyzenAdj non trovato", 21 | "WATTS":"Watt", 22 | "GPU_FREQMODE":"Modalità frequenza GPU", 23 | "UNLIMITED":"Illimitato", 24 | "FIXED_FREQ":"Fisso", 25 | "RANGE_FREQ":"Intervallo", 26 | "AUTO_FREQ":"Auto", 27 | "GPU_FIX_FREQ":"Frequenza GPU", 28 | "GPU_MIN_FREQ":"Frequenza minima", 29 | "GPU_MAX_FREQ":"Frequenza massima", 30 | "FAN_SPEED":"Velocità ventola", 31 | "CREATE_FAN_PROFILE":"Crea profilo ventola", 32 | "GRID_ALIG":"Allineamento griglia", 33 | "FAN_MODE":"Modalità ventola", 34 | "NOT_CONTROLLED":"Non controllato", 35 | "FIXED":"Fisso", 36 | "CURVE":"Curva", 37 | "SNAP_GRIDLINE":"Aggancia alle intersezioni della griglia", 38 | "FAN_SPEED_PERCENT":"Velocità ventola (%)", 39 | "SENSOR_TEMP":"Temperatura sensore", 40 | "CREATE_FAN_PROFILE_TIP":"Crea un profilo ventola", 41 | "SELECT_FAN_PROFILE_TIP":"Seleziona un profilo ventola", 42 | "FAN_PROFILE_NAME":"Nome profilo", 43 | "USE":"Usa", 44 | "DELETE":"Elimina", 45 | "CREATE":"Crea", 46 | "CANCEL":"Annulla", 47 | "CURENT_STAT":"Stato attuale", 48 | "EDIT":"Modifica", 49 | "SAVE":"Salva", 50 | "NATIVE_FREQ":"Nativo", 51 | "MORE":"Altro", 52 | "REINSTALL_PLUGIN": "Reinstalla plugin", 53 | "UPDATE_PLUGIN": "Aggiorna a", 54 | "ROLLBACK_PLUGIN": "Ripristina a", 55 | "INSTALLED_VERSION": "Versione installata", 56 | "LATEST_VERSION": "Ultima versione", 57 | "GPU_NATIVE_SLIDER": "Slider GPU nativo", 58 | "GPU_NATIVE_SLIDER_DESC": "Abilita lo slider di controllo GPU nativo", 59 | "USE_PERACMODE_PROFILE": "Usa profilo per modalità alimentazione", 60 | "AC_MODE": "Modalità AC", 61 | "BAT_MODE": "Modalità batteria", 62 | "CUSTOM_TDP_RANGE": "Intervallo TDP personalizzato", 63 | "RESET_ALL": "Ripristina tutto", 64 | "NATIVE_FREQ_DESC": "Imposta la frequenza tramite il menu di sistema", 65 | "UNLIMITED_DESC": "Nessun limite di frequenza GPU, pianificazione predefinita del sistema", 66 | "FIXED_FREQ_DESC": "Frequenza GPU fissa", 67 | "RANGE_FREQ_DESC": "Imposta intervallo di frequenza GPU", 68 | "AUTO_FREQ_DESC": "Frequenza GPU adattiva, limite TDP disabilitato forzatamente, Boost disabilitato", 69 | "AUTO_FREQ_TDP_NOTIF": "Modalità GPU {{mode}}, limite TDP disabilitato", 70 | "NATIVE_TDP_SLIDER": "Slider TDP nativo", 71 | "NATIVE_TDP_SLIDER_DESC": "Abilita il cursore TDP nativo, richiede il riavvio del sistema per rendere effettiva l'attivazione/disattivazione. La modifica del valore massimo dell'intervallo TDP richiede il riavvio di Steam per avere effetto. Dopo l'attivazione, le impostazioni TDP vengono salvate da Steam e le impostazioni dello stato di alimentazione nel plugin non gestiranno la configurazione TDP", 72 | "FORCE_SHOW_TDP": "Forza visualizzazione controllo TDP", 73 | "FORCE_SHOW_TDP_DESC": "Per impostazione predefinita, il plugin elabora lo slider TDP nativo. Se lo slider nativo presenta problemi, abilita questa opzione per utilizzare il controllo TDP interno del plugin", 74 | "MANUAL_BYPASS_CHARGE": "Carica bypass manuale", 75 | "MANUAL_BYPASS_CHARGE_DESC": "Controlla manualmente la carica bypass", 76 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "L'attivazione disabiliterà temporaneamente il limite di carica", 77 | "CHARGE_LIMIT": "Limite di carica", 78 | "CHARGE_LIMIT_DESC": "Imposta un limite di carica per prolungare la durata della batteria", 79 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "Carica bypass attiva, limite di carica temporaneamente disabilitato", 80 | "USE_OLD_UI": "Usa la vecchia interfaccia", 81 | "USE_OLD_UI_DESC": "Usa la vecchia visualizzazione a lista invece della nuova visualizzazione a schede" 82 | } -------------------------------------------------------------------------------- /src/i18n/japanese.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"設定", 3 | "ENABLE_SETTINGS":"設定を有効にする", 4 | "USE_PERGAME_PROFILE":"ゲームごとのプロファイルを使用", 5 | "USING":"使用中", 6 | "DEFAULT":"デフォルト", 7 | "PROFILE":"プロファイル", 8 | "CPU_BOOST":"CPU ブースト", 9 | "CPU_BOOST_DESC":"CPU の最大周波数を上げる", 10 | "HT_DESC":"Intel ハイパースレッディングを有効にする", 11 | "SMT_DESC":"同時マルチスレッディングを有効にする", 12 | "CPU_NUM":"CPU コア数", 13 | "CPU_NUM_DESC":"有効な物理コアを設定", 14 | "CPU_MAX_PERF":"CPU 最大パフォーマンス", 15 | "CPU_MAX_PERF_AUTO":"自動 CPU 最大パフォーマンス", 16 | "CPU_GOVERNOR": "CPU ガバナー", 17 | "CPU_GOVERNOR_DESC": "CPU パフォーマンススケジューリングポリシーを設定", 18 | "TDP":"熱設計電力 (TDP) 制限", 19 | "TDP_DESC":"総消費電力を抑えるためにプロセッサの電力を制限", 20 | "RYZENADJ_NOT_FOUND":"RyzenAdj が見つかりません", 21 | "WATTS":"ワット", 22 | "GPU_FREQMODE":"GPU クロック周波数モード", 23 | "UNLIMITED":"制限なし", 24 | "FIXED_FREQ":"固定", 25 | "RANGE_FREQ":"範囲", 26 | "AUTO_FREQ":"自動", 27 | "GPU_FIX_FREQ":"GPU クロック周波数", 28 | "GPU_MIN_FREQ":"最小周波数制限", 29 | "GPU_MAX_FREQ":"最大周波数制限", 30 | "FAN_SPEED":"ファン速度", 31 | "CREATE_FAN_PROFILE":"ファンプロファイルを作成", 32 | "GRID_ALIG":"グリッド配置", 33 | "FAN_MODE":"ファンモード", 34 | "NOT_CONTROLLED":"制御なし", 35 | "FIXED":"固定", 36 | "CURVE":"カーブ", 37 | "SNAP_GRIDLINE":"グリッド線の交点にスナップ", 38 | "FAN_SPEED_PERCENT":"ファン速度パーセント", 39 | "SENSOR_TEMP":"センサー温度", 40 | "CREATE_FAN_PROFILE_TIP":"ファンプロファイルを作成", 41 | "SELECT_FAN_PROFILE_TIP":"ファンプロファイルを選択", 42 | "FAN_PROFILE_NAME":"プロファイル名", 43 | "USE":"使用", 44 | "DELETE":"削除", 45 | "CREATE":"作成", 46 | "CANCEL":"キャンセル", 47 | "CURENT_STAT":"現在の状態", 48 | "EDIT":"編集", 49 | "SAVE":"保存", 50 | "NATIVE_FREQ":"ネイティブ", 51 | "MORE":"その他", 52 | "REINSTALL_PLUGIN": "プラグインを再インストール", 53 | "UPDATE_PLUGIN": "更新:", 54 | "ROLLBACK_PLUGIN": "ロールバック:", 55 | "INSTALLED_VERSION": "インストール済みバージョン", 56 | "LATEST_VERSION": "最新バージョン", 57 | "GPU_NATIVE_SLIDER": "ネイティブ GPU スライダー", 58 | "GPU_NATIVE_SLIDER_DESC": "ネイティブ GPU 制御スライダーを有効にする", 59 | "USE_PERACMODE_PROFILE": "電源モードごとのプロファイルを使用", 60 | "AC_MODE": "AC モード", 61 | "BAT_MODE": "バッテリーモード", 62 | "CUSTOM_TDP_RANGE": "カスタム TDP スライダー範囲", 63 | "RESET_ALL": "すべてリセット", 64 | "NATIVE_FREQ_DESC": "システムショートカットメニューで周波数を設定", 65 | "UNLIMITED_DESC": "GPU 周波数制限なし、システムデフォルトのスケジューリング", 66 | "FIXED_FREQ_DESC": "GPU 周波数を固定", 67 | "RANGE_FREQ_DESC": "GPU 周波数範囲を設定", 68 | "AUTO_FREQ_DESC": "GPU 周波数を自動調整、TDP 制限を強制的に無効化、ブーストを無効化", 69 | "AUTO_FREQ_TDP_NOTIF": "GPU モード {{mode}}、TDP 制限無効", 70 | "NATIVE_TDP_SLIDER": "ネイティブ TDP スライダー", 71 | "NATIVE_TDP_SLIDER_DESC": "ネイティブTDPスライダーを有効化、有効/無効の切り替えにはシステムの再起動が必要です。TDP範囲の最大値を変更するにはSteamの再起動が必要です。有効化後、TDP設定はSteamによって保存され、プラグイン設定の電源状態設定ではTDP設定を処理しません", 72 | "FORCE_SHOW_TDP": "TDP 制御を強制表示", 73 | "FORCE_SHOW_TDP_DESC": "デフォルトでは、プラグインはネイティブ TDP スライダーを処理します。ネイティブスライダーに問題がある場合、このオプションを有効にしてプラグイン内部の TDP 制御を使用してください", 74 | "MANUAL_BYPASS_CHARGE": "手動バイパス充電", 75 | "MANUAL_BYPASS_CHARGE_DESC": "バイパス充電を手動で制御", 76 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "有効にすると充電制限が一時的に無効になります", 77 | "CHARGE_LIMIT": "充電制限", 78 | "CHARGE_LIMIT_DESC": "バッテリー寿命を延ばすために充電制限を設定", 79 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "手動バイパス充電が有効、充電制限は一時的に無効", 80 | "USE_OLD_UI": "旧UIを使用", 81 | "USE_OLD_UI_DESC": "新しいタブビューの代わりに旧リストビューを使用する" 82 | } -------------------------------------------------------------------------------- /src/i18n/koreana.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"설정", 3 | "ENABLE_SETTINGS":"설정 활성화", 4 | "USE_PERGAME_PROFILE":"게임별 프로필 사용", 5 | "USING":"사용 중", 6 | "DEFAULT":"기본값", 7 | "PROFILE":"프로필", 8 | "CPU_BOOST":"CPU 부스트", 9 | "CPU_BOOST_DESC":"최대 CPU 주파수 증가", 10 | "HT_DESC":"인텔 하이퍼스레딩 활성화", 11 | "SMT_DESC":"동시 다중 스레딩 활성화", 12 | "CPU_NUM":"CPU 코어 수", 13 | "CPU_NUM_DESC":"활성화할 물리적 코어 설정", 14 | "CPU_MAX_PERF":"CPU 최대 성능", 15 | "CPU_MAX_PERF_AUTO":"자동 CPU 최대 성능", 16 | "CPU_GOVERNOR": "CPU 거버너", 17 | "CPU_GOVERNOR_DESC": "CPU 성능 스케줄링 정책 설정", 18 | "TDP":"열 설계 전력 (TDP) 제한", 19 | "TDP_DESC":"전체 전력 소비를 줄이기 위해 프로세서 전력 제한", 20 | "RYZENADJ_NOT_FOUND":"RyzenAdj를 찾을 수 없음", 21 | "WATTS":"와트", 22 | "GPU_FREQMODE":"GPU 클럭 주파수 모드", 23 | "UNLIMITED":"제한 없음", 24 | "FIXED_FREQ":"고정", 25 | "RANGE_FREQ":"범위", 26 | "AUTO_FREQ":"자동", 27 | "GPU_FIX_FREQ":"GPU 클럭 주파수", 28 | "GPU_MIN_FREQ":"최소 주파수 제한", 29 | "GPU_MAX_FREQ":"최대 주파수 제한", 30 | "FAN_SPEED":"팬 속도", 31 | "CREATE_FAN_PROFILE":"팬 프로필 생성", 32 | "GRID_ALIG":"그리드 정렬", 33 | "FAN_MODE":"팬 모드", 34 | "NOT_CONTROLLED":"제어 안 함", 35 | "FIXED":"고정", 36 | "CURVE":"곡선", 37 | "SNAP_GRIDLINE":"그리드 선 교차점에 스냅", 38 | "FAN_SPEED_PERCENT":"팬 속도 백분율", 39 | "SENSOR_TEMP":"센서 온도", 40 | "CREATE_FAN_PROFILE_TIP":"팬 프로필 생성", 41 | "SELECT_FAN_PROFILE_TIP":"팬 프로필 선택", 42 | "FAN_PROFILE_NAME":"프로필 이름", 43 | "USE":"사용", 44 | "DELETE":"삭제", 45 | "CREATE":"생성", 46 | "CANCEL":"취소", 47 | "CURENT_STAT":"현재 상태", 48 | "EDIT":"편집", 49 | "SAVE":"저장", 50 | "NATIVE_FREQ":"기본", 51 | "MORE":"더 보기", 52 | "REINSTALL_PLUGIN": "플러그인 재설치", 53 | "UPDATE_PLUGIN": "업데이트:", 54 | "ROLLBACK_PLUGIN": "롤백:", 55 | "INSTALLED_VERSION": "설치된 버전", 56 | "LATEST_VERSION": "최신 버전", 57 | "GPU_NATIVE_SLIDER": "기본 GPU 슬라이더", 58 | "GPU_NATIVE_SLIDER_DESC": "기본 GPU 제어 슬라이더 활성화", 59 | "USE_PERACMODE_PROFILE": "전원 모드별 프로필 사용", 60 | "AC_MODE": "AC 모드", 61 | "BAT_MODE": "배터리 모드", 62 | "CUSTOM_TDP_RANGE": "사용자 정의 TDP 슬라이더 범위", 63 | "RESET_ALL": "모두 초기화", 64 | "NATIVE_FREQ_DESC": "시스템 단축 메뉴로 주파수 설정", 65 | "UNLIMITED_DESC": "GPU 주파수 제한 없음, 시스템 기본 스케줄링", 66 | "FIXED_FREQ_DESC": "GPU 주파수 고정", 67 | "RANGE_FREQ_DESC": "GPU 주파수 범위 설정", 68 | "AUTO_FREQ_DESC": "GPU 주파수 자동 조정, TDP 제한 강제 비활성화, 부스트 비활성화", 69 | "AUTO_FREQ_TDP_NOTIF": "GPU 모드 {{mode}}, TDP 제한 비활성화", 70 | "NATIVE_TDP_SLIDER": "기본 TDP 슬라이더", 71 | "NATIVE_TDP_SLIDER_DESC": "기본 TDP 슬라이더 활성화, 활성화/비활성화에는 시스템 재시작이 필요합니다. TDP 범위 최대값 수정에는 Steam 재시작이 필요합니다. 활성화 후 TDP 설정은 Steam에서 저장되며, 플러그인 설정의 전원 상태 설정에서는 TDP 설정을 처리하지 않습니다", 72 | "FORCE_SHOW_TDP": "TDP 제어 강제 표시", 73 | "FORCE_SHOW_TDP_DESC": "기본적으로 플러그인은 기본 TDP 슬라이더를 처리합니다. 기본 슬라이더에 문제가 있는 경우 이 옵션을 활성화하여 플러그인 내부 TDP 제어 사용", 74 | "MANUAL_BYPASS_CHARGE": "우회 충전 수동 제어", 75 | "MANUAL_BYPASS_CHARGE_DESC": "우회 충전을 수동으로 제어합니다", 76 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "활성화하면 충전 제한이 일시적으로 비활성화됩니다", 77 | "CHARGE_LIMIT": "충전 제한", 78 | "CHARGE_LIMIT_DESC": "배터리 수명을 연장하기 위한 충전 제한 설정", 79 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "우회 충전이 활성화되어 충전 제한이 일시적으로 비활성화됨", 80 | "USE_OLD_UI": "이전 UI 사용", 81 | "USE_OLD_UI_DESC": "새로운 탭 보기 대신 이전 목록 보기 사용" 82 | } -------------------------------------------------------------------------------- /src/i18n/localization.ts: -------------------------------------------------------------------------------- 1 | import { defaultLocale, localizeMap, LocalizeStrKey } from "./localizeMap"; 2 | 3 | import i18n, { Resource} from "i18next"; 4 | 5 | export class localizationManager { 6 | private static language = "english"; 7 | 8 | public static async init() { 9 | const language = 10 | (await SteamClient.Settings.GetCurrentLanguage()) || "english"; 11 | this.language = language; 12 | console.log("Language: " + this.language); 13 | 14 | const resources: Resource = Object.keys(localizeMap).reduce( 15 | (acc: Resource, key) => { 16 | acc[localizeMap[key].locale] = { 17 | translation: localizeMap[key].strings, 18 | }; 19 | return acc; 20 | }, 21 | {} 22 | ); 23 | 24 | i18n.init({ 25 | resources: resources, 26 | lng: this.getLocale(), // 目标语言 27 | fallbackLng: defaultLocale, // 回落语言 28 | returnEmptyString: false, // 空字符串不返回, 使用回落语言 29 | interpolation: { 30 | escapeValue: false, 31 | }, 32 | }); 33 | } 34 | 35 | private static getLocale() { 36 | return localizeMap[this.language]?.locale ?? defaultLocale; 37 | } 38 | 39 | public static getString( 40 | defaultString: LocalizeStrKey, 41 | variables?: Record 42 | ) { 43 | return i18n.t(defaultString, variables); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/i18n/localizeMap.ts: -------------------------------------------------------------------------------- 1 | import * as schinese from "./schinese.json"; 2 | import * as tchinese from "./tchinese.json"; 3 | import * as english from "./english.json"; 4 | import * as german from "./german.json"; 5 | import * as japanese from "./japanese.json"; 6 | import * as koreana from "./koreana.json"; 7 | import * as thai from "./thai.json"; 8 | import * as bulgarian from "./bulgarian.json"; 9 | import * as italian from "./italian.json"; 10 | import * as french from "./french.json"; 11 | 12 | export interface LanguageProps { 13 | label: string; 14 | strings: any; 15 | credit: string[]; 16 | locale: string; 17 | } 18 | 19 | export const defaultLanguage = "english"; 20 | export const defaultLocale = "en"; 21 | export const defaultMessages = english; 22 | 23 | export const localizeMap: { [key: string]: LanguageProps } = { 24 | schinese: { 25 | label: "简体中文", 26 | strings: schinese, 27 | credit: ["yxx"], 28 | locale: "zh-CN", 29 | }, 30 | tchinese: { 31 | label: "繁體中文", 32 | strings: tchinese, 33 | credit: [], 34 | locale: "zh-TW", 35 | }, 36 | english: { 37 | label: "English", 38 | strings: english, 39 | credit: [], 40 | locale: "en", 41 | }, 42 | german: { 43 | label: "Deutsch", 44 | strings: german, 45 | credit: ["dctr"], 46 | locale: "de", 47 | }, 48 | japanese: { 49 | label: "日本語", 50 | strings: japanese, 51 | credit: [], 52 | locale: "ja", 53 | }, 54 | koreana: { 55 | label: "한국어", 56 | strings: koreana, 57 | credit: [], 58 | locale: "ko", 59 | }, 60 | thai: { 61 | label: "ไทย", 62 | strings: thai, 63 | credit: [], 64 | locale: "th", 65 | }, 66 | bulgarian: { 67 | label: "Български", 68 | strings: bulgarian, 69 | credit: [], 70 | locale: "bg", 71 | }, 72 | italian: { 73 | label: "Italiano", 74 | strings: italian, 75 | credit: [], 76 | locale: "it", 77 | }, 78 | french: { 79 | label: "Français", 80 | strings: french, 81 | credit: [], 82 | locale: "fr", 83 | }, 84 | }; 85 | 86 | // 创建一个类型安全的常量生成函数 87 | function createLocalizeConstants(keys: T) { 88 | return keys.reduce((obj, key) => { 89 | obj[key as keyof typeof obj] = key; 90 | return obj; 91 | }, {} as { [K in T[number]]: K }); 92 | } 93 | 94 | // 定义所有键名 95 | const I18N_KEYS = [ 96 | "TITEL_SETTINGS", 97 | "ENABLE_SETTINGS", 98 | "USE_PERGAME_PROFILE", 99 | "USING", 100 | "DEFAULT", 101 | "PROFILE", 102 | "CPU_BOOST", 103 | "CPU_BOOST_DESC", 104 | "HT_DESC", 105 | "SMT_DESC", 106 | "CPU_NUM", 107 | "CPU_NUM_DESC", 108 | "CPU_MAX_PERF", 109 | "CPU_MAX_PERF_AUTO", 110 | "TDP", 111 | "TDP_DESC", 112 | "RYZENADJ_NOT_FOUND", 113 | "WATTS", 114 | "GPU_FREQMODE", 115 | "UNLIMITED", 116 | "FIXED_FREQ", 117 | "RANGE_FREQ", 118 | "AUTO_FREQ", 119 | "GPU_FIX_FREQ", 120 | "GPU_MIN_FREQ", 121 | "GPU_MAX_FREQ", 122 | "FAN_SPEED", 123 | "CREATE_FAN_PROFILE", 124 | "GRID_ALIG", 125 | "FAN_MODE", 126 | "NOT_CONTROLLED", 127 | "FIXED", 128 | "CURVE", 129 | "SNAP_GRIDLINE", 130 | "FAN_SPEED_PERCENT", 131 | "SENSOR_TEMP", 132 | "CREATE_FAN_PROFILE_TIP", 133 | "SELECT_FAN_PROFILE_TIP", 134 | "FAN_PROFILE_NAME", 135 | "USE", 136 | "DELETE", 137 | "CREATE", 138 | "CANCEL", 139 | "CURENT_STAT", 140 | "EDIT", 141 | "SAVE", 142 | "NATIVE_FREQ", 143 | "MORE", 144 | "REINSTALL_PLUGIN", 145 | "UPDATE_PLUGIN", 146 | "ROLLBACK_PLUGIN", 147 | "INSTALLED_VERSION", 148 | "LATEST_VERSION", 149 | "GPU_NATIVE_SLIDER", 150 | "GPU_NATIVE_SLIDER_DESC", 151 | "USE_PERACMODE_PROFILE", 152 | "AC_MODE", 153 | "BAT_MODE", 154 | "CUSTOM_TDP_RANGE", 155 | "RESET_ALL", 156 | "NATIVE_FREQ_DESC", 157 | "UNLIMITED_DESC", 158 | "FIXED_FREQ_DESC", 159 | "RANGE_FREQ_DESC", 160 | "AUTO_FREQ_DESC", 161 | "AUTO_FREQ_TDP_NOTIF", 162 | "NATIVE_TDP_SLIDER", 163 | "NATIVE_TDP_SLIDER_DESC", 164 | "CPU_GOVERNOR", 165 | "CPU_GOVERNOR_DESC", 166 | "CPU_EPP", 167 | "CPU_EPP_DESC", 168 | "MANUAL_BYPASS_CHARGE", 169 | "MANUAL_BYPASS_CHARGE_DESC", 170 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT", 171 | "CHARGE_LIMIT", 172 | "CHARGE_LIMIT_DESC", 173 | "CHARGE_LIMIT_DESC_WITH_BYPASS", 174 | "USE_OLD_UI", 175 | "USE_OLD_UI_DESC", 176 | ] as const; 177 | 178 | // 创建常量对象并导出 179 | export const L = createLocalizeConstants(I18N_KEYS); 180 | 181 | // 导出类型 182 | export type LocalizeStrKey = keyof typeof L; 183 | 184 | // 为了向后兼容,保留 localizeStrEnum 名称 185 | export const localizeStrEnum = L; 186 | -------------------------------------------------------------------------------- /src/i18n/schinese.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"设置", 3 | "ENABLE_SETTINGS":"启用插件设置", 4 | "USE_PERGAME_PROFILE":"使用按游戏设置的配置文件", 5 | "USING":"正在使用", 6 | "DEFAULT":"默认", 7 | "PROFILE":"配置文件", 8 | "CPU_BOOST":"睿 频", 9 | "CPU_BOOST_DESC":"提升最大cpu频率", 10 | "HT_DESC":"启用超线程", 11 | "SMT_DESC":"启用同步多线程", 12 | "CPU_NUM":"核 心 数", 13 | "CPU_NUM_DESC":"设置启用的物理核心数量", 14 | "CPU_MAX_PERF":"最大性能百分比", 15 | "CPU_MAX_PERF_AUTO":"自动最大性能百分比", 16 | "CPU_GOVERNOR": "CPU 调度器", 17 | "CPU_GOVERNOR_DESC": "选择 CPU 频率调度器", 18 | "CPU_EPP": "能耗性能偏好", 19 | "CPU_EPP_DESC": "在节能和性能之间进行平衡", 20 | "TDP":"热设计功耗 (TDP) 限制", 21 | "TDP_DESC":"限制处理器功耗以降低总功耗", 22 | "RYZENADJ_NOT_FOUND":"未检测到ryzenAdj", 23 | "WATTS":"瓦特", 24 | "GPU_FREQMODE":"GPU 频率模式", 25 | "UNLIMITED":"不限制", 26 | "FIXED_FREQ":"固定频率", 27 | "RANGE_FREQ":"范围频率", 28 | "AUTO_FREQ":"自适应", 29 | "GPU_FIX_FREQ":"GPU 频率", 30 | "GPU_MIN_FREQ":"GPU 最小频率限制", 31 | "GPU_MAX_FREQ":"GPU 最大频率限制", 32 | "FAN_SPEED":"风扇转速", 33 | "CREATE_FAN_PROFILE":"创建风扇配置文件", 34 | "GRID_ALIG":"网格对齐", 35 | "FAN_MODE":"风扇模式", 36 | "NOT_CONTROLLED":"系统", 37 | "FIXED":"固定", 38 | "CURVE":"曲线", 39 | "SNAP_GRIDLINE":"对齐到网格线交点", 40 | "FAN_SPEED_PERCENT":"风扇转速百分比", 41 | "SENSOR_TEMP":"传感器温度", 42 | "CREATE_FAN_PROFILE_TIP":"创建一个风扇配置文件", 43 | "SELECT_FAN_PROFILE_TIP":"选择一个风扇配置文件", 44 | "FAN_PROFILE_NAME":"配置文件名称", 45 | "USE":"使用", 46 | "DELETE":"删除", 47 | "CREATE":"创建", 48 | "CANCEL":"取消", 49 | "CURENT_STAT":"当前状态", 50 | "EDIT":"编辑", 51 | "SAVE":"保存", 52 | "NATIVE_FREQ":"原生设置", 53 | "MORE":"更多", 54 | "REINSTALL_PLUGIN": "重新安装插件", 55 | "UPDATE_PLUGIN": "更新到", 56 | "ROLLBACK_PLUGIN": "回滚到", 57 | "INSTALLED_VERSION": "当前版本", 58 | "LATEST_VERSION": "最新版本", 59 | "GPU_NATIVE_SLIDER": "原生控制条", 60 | "GPU_NATIVE_SLIDER_DESC": "修复并启用原生控制条", 61 | "USE_PERACMODE_PROFILE": "使用按充电状态设置的配置文件", 62 | "AC_MODE": "充电模式", 63 | "BAT_MODE": "电池模式", 64 | "CUSTOM_TDP_RANGE": "自定义 TDP 滑块范围(超出 BIOS 上限的值无效)", 65 | "RESET_ALL": "重置所有设置", 66 | "NATIVE_FREQ_DESC": "使用系统快捷菜单频率设置", 67 | "UNLIMITED_DESC": "不限制 GPU 频率, 系统默认调度", 68 | "FIXED_FREQ_DESC": "固定 GPU 频率", 69 | "RANGE_FREQ_DESC": "设置 GPU 频率范围", 70 | "AUTO_FREQ_DESC": "自适应 GPU 频率, 强制关闭 TDP 限制, 关闭 Boost", 71 | "AUTO_FREQ_TDP_NOTIF": "GPU 模式为{{mode}}, 关闭 TDP 限制", 72 | "NATIVE_TDP_SLIDER": "原生 TDP 滑块", 73 | "NATIVE_TDP_SLIDER_DESC": "启用原生 TDP 滑块, 启用和关闭需要重启系统生效。修改 TDP 范围最大值需要重启 Steam 生效。开启后 TDP 配置由 Steam 保存, 插件设置中的电源状态配置将不会处理 TDP 配置", 74 | "FORCE_SHOW_TDP": "强制显示 TDP 控制", 75 | "FORCE_SHOW_TDP_DESC": "默认情况下插件会对原生 TDP 滑动条进行处理, 如果原生滑动条异常, 打开此项使用插件内 TDP 控制", 76 | "MANUAL_BYPASS_CHARGE": "手动旁路供电", 77 | "MANUAL_BYPASS_CHARGE_DESC": "手动控制旁路供电开关", 78 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "开启后将暂时禁用充电限制功能", 79 | "CHARGE_LIMIT": "充电限制", 80 | "CHARGE_LIMIT_DESC": "设置电池充电限制,延长电池寿命", 81 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "手动旁路供电已开启,充电限制暂时无效", 82 | "USE_OLD_UI": "使用旧版 UI", 83 | "USE_OLD_UI_DESC": "使用旧的列表视图而不是新的标签视图" 84 | } -------------------------------------------------------------------------------- /src/i18n/tchinese.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"設定", 3 | "ENABLE_SETTINGS":"啟用設定", 4 | "USE_PERGAME_PROFILE":"依遊戲設定檔案", 5 | "USING":"正在使用", 6 | "DEFAULT":"預設", 7 | "PROFILE":"設定檔", 8 | "CPU_BOOST":"超頻模式", 9 | "CPU_BOOST_DESC":"提升最大 CPU 頻率", 10 | "HT_DESC":"啟用超執行緒", 11 | "SMT_DESC":"啟用同時多執行緒", 12 | "CPU_NUM":"CPU 核心數", 13 | "CPU_NUM_DESC":"設定啟用的實體核心數", 14 | "CPU_MAX_PERF":"最大效能百分比", 15 | "CPU_MAX_PERF_AUTO":"自動最大效能百分比", 16 | "CPU_GOVERNOR": "CPU 調度器", 17 | "CPU_GOVERNOR_DESC": "設置 CPU 效能調度策略", 18 | "TDP":"熱設計功耗 (TDP) 限制", 19 | "TDP_DESC":"限制處理器功耗以降低總功耗", 20 | "RYZENADJ_NOT_FOUND":"未偵測到 RyzenAdj", 21 | "WATTS":"瓦特", 22 | "GPU_FREQMODE":"GPU 頻率模式", 23 | "UNLIMITED":"不限制", 24 | "FIXED_FREQ":"固定", 25 | "RANGE_FREQ":"範圍", 26 | "AUTO_FREQ":"自動調整", 27 | "GPU_FIX_FREQ":"GPU 頻率", 28 | "GPU_MIN_FREQ":"最低頻率", 29 | "GPU_MAX_FREQ":"最高頻率", 30 | "FAN_SPEED":"風扇轉速", 31 | "CREATE_FAN_PROFILE":"建立風扇設定檔", 32 | "GRID_ALIG":"格線對齊", 33 | "FAN_MODE":"風扇模式", 34 | "NOT_CONTROLLED":"不控制", 35 | "FIXED":"固定", 36 | "CURVE":"曲線", 37 | "SNAP_GRIDLINE":"對齊至格線交點", 38 | "FAN_SPEED_PERCENT":"風扇轉速百分比", 39 | "SENSOR_TEMP":"感測器溫度", 40 | "CREATE_FAN_PROFILE_TIP":"建立風扇設定檔", 41 | "SELECT_FAN_PROFILE_TIP":"選擇風扇設定檔", 42 | "FAN_PROFILE_NAME":"設定檔名稱", 43 | "USE":"使用", 44 | "DELETE":"刪除", 45 | "CREATE":"建立", 46 | "CANCEL":"取消", 47 | "CURENT_STAT":"目前狀態", 48 | "EDIT":"編輯", 49 | "SAVE":"儲存", 50 | "NATIVE_FREQ":"原生設定", 51 | "MORE":"更多", 52 | "REINSTALL_PLUGIN": "重新安裝外掛程式", 53 | "UPDATE_PLUGIN": "更新至", 54 | "ROLLBACK_PLUGIN": "還原至", 55 | "INSTALLED_VERSION": "目前版本", 56 | "LATEST_VERSION": "最新版本", 57 | "GPU_NATIVE_SLIDER": "原生 GPU 控制滑桿", 58 | "GPU_NATIVE_SLIDER_DESC": "啟用原生 GPU 控制滑桿", 59 | "USE_PERACMODE_PROFILE": "依電源模式設定檔", 60 | "AC_MODE": "外接電源模式", 61 | "BAT_MODE": "電池模式", 62 | "CUSTOM_TDP_RANGE": "自訂 TDP 滑桿範圍", 63 | "RESET_ALL": "重設所有設定", 64 | "NATIVE_FREQ_DESC": "透過系統快速選單設定頻率", 65 | "UNLIMITED_DESC": "不限制 GPU 頻率,使用系統預設排程", 66 | "FIXED_FREQ_DESC": "固定 GPU 頻率", 67 | "RANGE_FREQ_DESC": "設定 GPU 頻率範圍", 68 | "AUTO_FREQ_DESC": "GPU 頻率自動調整,強制關閉 TDP 限制,關閉超頻", 69 | "AUTO_FREQ_TDP_NOTIF": "GPU 模式 {{mode}},TDP 限制已關閉", 70 | "NATIVE_TDP_SLIDER": "原生 TDP 滑桿", 71 | "NATIVE_TDP_SLIDER_DESC": "啟用原生 TDP 滑桿, 啟用和關閉需要重啟系統生效。修改 TDP 範圍最大值需要重啟 Steam 生效。開啟後 TDP 設定由 Steam 儲存, 外掛程式設定中的電源狀態設定將不會處理 TDP 設定", 72 | "FORCE_SHOW_TDP": "強制顯示 TDP 控制", 73 | "FORCE_SHOW_TDP_DESC": "預設情況下外掛程式會處理原生 TDP 滑桿。如果原生滑桿有問題,請開啟此選項以使用外掛程式內建的 TDP 控制", 74 | "MANUAL_BYPASS_CHARGE": "手動旁路供電", 75 | "MANUAL_BYPASS_CHARGE_DESC": "手動控制旁路供電開關", 76 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "開啟後將暫時禁用充電限制功能", 77 | "CHARGE_LIMIT": "充電限制", 78 | "CHARGE_LIMIT_DESC": "設置電池充電限制,延長電池壽命", 79 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "手動旁路供電已開啟,充電限制暫時無效", 80 | "USE_OLD_UI": "使用舊版 UI", 81 | "USE_OLD_UI_DESC": "使用舊的列表視圖而不是新的標籤視圖" 82 | } -------------------------------------------------------------------------------- /src/i18n/thai.json: -------------------------------------------------------------------------------- 1 | { 2 | "TITEL_SETTINGS":"การตั้งค่า", 3 | "ENABLE_SETTINGS":"เปิดใช้งานการตั้งค่า", 4 | "USE_PERGAME_PROFILE":"ใช้โปรไฟล์ตามเกม", 5 | "USING":"กำลังใช้งาน", 6 | "DEFAULT":"ค่าเริ่มต้น", 7 | "PROFILE":"โปรไฟล์", 8 | "CPU_BOOST":"เพิ่มประสิทธิภาพ CPU", 9 | "CPU_BOOST_DESC":"เพิ่มความถี่สูงสุดของ CPU", 10 | "HT_DESC":"เปิดใช้งาน Intel Hyper-Threading", 11 | "SMT_DESC":"เปิดใช้งานการประมวลผลแบบหลายเธรดพร้อมกัน", 12 | "CPU_NUM":"จำนวนคอร์ CPU", 13 | "CPU_NUM_DESC":"กำหนดคอร์กายภาพที่เปิดใช้งาน", 14 | "CPU_MAX_PERF":"ประสิทธิภาพสูงสุดของ CPU", 15 | "CPU_MAX_PERF_AUTO":"ประสิทธิภาพสูงสุดของ CPU อัตโนมัติ", 16 | "CPU_GOVERNOR": "ตัวควบคุม CPU", 17 | "CPU_GOVERNOR_DESC": "ตั้งค่านโยบายการจัดการประสิทธิภาพ CPU", 18 | "TDP":"ขีดจำกัดพลังงานความร้อน (TDP)", 19 | "TDP_DESC":"จำกัดกำลังของโปรเซสเซอร์เพื่อลดการใช้พลังงานรวม", 20 | "RYZENADJ_NOT_FOUND":"ไม่พบ RyzenAdj", 21 | "WATTS":"วัตต์", 22 | "GPU_FREQMODE":"โหมดความถี่ GPU", 23 | "UNLIMITED":"ไม่จำกัด", 24 | "FIXED_FREQ":"คงที่", 25 | "RANGE_FREQ":"ช่วง", 26 | "AUTO_FREQ":"อัตโนมัติ", 27 | "GPU_FIX_FREQ":"ความถี่ GPU", 28 | "GPU_MIN_FREQ":"ความถี่ต่ำสุด", 29 | "GPU_MAX_FREQ":"ความถี่สูงสุด", 30 | "FAN_SPEED":"ความเร็วพัดลม", 31 | "CREATE_FAN_PROFILE":"สร้างโปรไฟล์พัดลม", 32 | "GRID_ALIG":"การจัดตำแหน่งตาราง", 33 | "FAN_MODE":"โหมดพัดลม", 34 | "NOT_CONTROLLED":"ไม่ควบคุม", 35 | "FIXED":"คงที่", 36 | "CURVE":"เส้นโค้ง", 37 | "SNAP_GRIDLINE":"ดึงดูดไปที่จุดตัดของเส้นตาราง", 38 | "FAN_SPEED_PERCENT":"ความเร็วพัดลม (%)", 39 | "SENSOR_TEMP":"อุณหภูมิเซ็นเซอร์", 40 | "CREATE_FAN_PROFILE_TIP":"สร้างโปรไฟล์พัดลม", 41 | "SELECT_FAN_PROFILE_TIP":"เลือกโปรไฟล์พัดลม", 42 | "FAN_PROFILE_NAME":"ชื่อโปรไฟล์", 43 | "USE":"ใช้", 44 | "DELETE":"ลบ", 45 | "CREATE":"สร้าง", 46 | "CANCEL":"ยกเลิก", 47 | "CURENT_STAT":"สถานะปัจจุบัน", 48 | "EDIT":"แก้ไข", 49 | "SAVE":"บันทึก", 50 | "NATIVE_FREQ":"ดั้งเดิม", 51 | "MORE":"เพิ่มเติม", 52 | "REINSTALL_PLUGIN": "ติดตั้งปลั๊กอินใหม่", 53 | "UPDATE_PLUGIN": "อัปเดตเป็น", 54 | "ROLLBACK_PLUGIN": "ย้อนกลับเป็น", 55 | "INSTALLED_VERSION": "เวอร์ชันที่ติดตั้ง", 56 | "LATEST_VERSION": "เวอร์ชันล่าสุด", 57 | "GPU_NATIVE_SLIDER": "แถบเลื่อน GPU ดั้งเดิม", 58 | "GPU_NATIVE_SLIDER_DESC": "เปิดใช้งานแถบเลื่อนควบคุม GPU ดั้งเดิม", 59 | "USE_PERACMODE_PROFILE": "ใช้โปรไฟล์ตามโหมดพลังงาน", 60 | "AC_MODE": "โหมดไฟฟ้า", 61 | "BAT_MODE": "โหมดแบตเตอรี่", 62 | "CUSTOM_TDP_RANGE": "ช่วง TDP แบบกำหนดเอง", 63 | "RESET_ALL": "รีเซ็ตทั้งหมด", 64 | "NATIVE_FREQ_DESC": "ตั้งค่าความถี่ผ่านเมนูระบบ", 65 | "UNLIMITED_DESC": "ไม่จำกัดความถี่ GPU, ใช้การจัดการระบบเริ่มต้น", 66 | "FIXED_FREQ_DESC": "ความถี่ GPU คงที่", 67 | "RANGE_FREQ_DESC": "กำหนดช่วงความถี่ GPU", 68 | "AUTO_FREQ_DESC": "ความถี่ GPU แบบปรับตัว, บังคับปิดการจำกัด TDP, ปิด Boost", 69 | "AUTO_FREQ_TDP_NOTIF": "โหมด GPU {{mode}}, ปิดการจำกัด TDP", 70 | "NATIVE_TDP_SLIDER": "แถบเลื่อน TDP ดั้งเดิม", 71 | "NATIVE_TDP_SLIDER_DESC": "เปิดใช้งานแถบเลื่อน TDP แบบดั้งเดิม ต้องรีสตาร์ทระบบเพื่อให้การเปิด/ปิดมีผล การแก้ไขค่าสูงสุดของช่วง TDP ต้องรีสตาร์ท Steam เพื่อให้มีผล หลังจากเปิดใช้งาน การตั้งค่า TDP จะถูกบันทึกโดย Steam และการตั้งค่าสถานะพลังงานในปลั๊กอินจะไม่จัดการการตั้งค่า TDP", 72 | "FORCE_SHOW_TDP": "บังคับแสดงการควบคุม TDP", 73 | "FORCE_SHOW_TDP_DESC": "โดยค่าเริ่มต้น ปลั๊กอินจะประมวลผลแถบเลื่อน TDP ดั้งเดิม หากแถบเลื่อนดั้งเดิมมีปัญหา ให้เปิดใช้งานตัวเลือกนี้เพื่อใช้การควบคุม TDP ภายในของปลั๊กอิน", 74 | "MANUAL_BYPASS_CHARGE": "ควบคุมการชาร์จแบบบายพาสด้วยตนเอง", 75 | "MANUAL_BYPASS_CHARGE_DESC": "ควบคุมการชาร์จแบบบายพาสด้วยตนเอง", 76 | "MANUAL_BYPASS_CHARGE_DESC_WITH_LIMIT": "การเปิดใช้งานจะปิดการจำกัดการชาร์จชั่วคราว", 77 | "CHARGE_LIMIT": "จำกัดการชาร์จ", 78 | "CHARGE_LIMIT_DESC": "ตั้งค่าการจำกัดการชาร์จเพื่อยืดอายุแบตเตอรี่", 79 | "CHARGE_LIMIT_DESC_WITH_BYPASS": "เปิดใช้งานการชาร์จแบบบายพาสแล้ว การจำกัดการชาร์จถูกปิดชั่วคราว", 80 | "USE_OLD_UI": "ใช้ UI แบบเก่า", 81 | "USE_OLD_UI_DESC": "ใช้มุมมองรายการแบบเก่าแทนมุมมองแท็บใหม่" 82 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (C) 2022 Sefa Eyeoglu (https://scrumplex.net) 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | import { 19 | definePlugin, 20 | PanelSectionRow, 21 | staticClasses, 22 | SteamSpinner, 23 | Tabs, 24 | } from "@decky/ui"; 25 | import { FC, useEffect, useMemo, useState } from "react"; 26 | import { FaFan, FaLayerGroup, FaSuperpowers } from "react-icons/fa"; 27 | import { 28 | Backend, 29 | ComponentName, 30 | PluginManager, 31 | Settings, 32 | UpdateType, 33 | } from "./util"; 34 | import { 35 | GPUComponent, 36 | CPUComponent, 37 | SettingsComponent, 38 | FANComponent, 39 | MoreComponent, 40 | QuickAccessTitleView, 41 | PowerComponent, 42 | } from "./components"; 43 | import { TabCpu, TabGpu, TabPower, TabMore, TabFans } from "./tab"; 44 | import { BsCpuFill } from "react-icons/bs"; 45 | import { PiGraphicsCardFill, PiLightningFill } from "react-icons/pi"; 46 | 47 | const ListView: FC<{}> = ({}) => { 48 | return ( 49 | <> 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | ); 58 | }; 59 | 60 | const TabView: FC<{ show?: boolean }> = ({ show = true }) => { 61 | const [currentTabRoute, setCurrentTabRoute] = useState( 62 | Settings.currentTabRoute 63 | ); 64 | 65 | const updateCurrentTabRoute = (route: string) => { 66 | setCurrentTabRoute(route); 67 | Settings.currentTabRoute = route; 68 | }; 69 | 70 | const supportChargeLimit = useMemo(() => { 71 | return Backend.data.getIsSupportChargeLimit(); 72 | }, []); 73 | 74 | const isSupportSoftwareChargeLimit = useMemo(() => { 75 | return Backend.data.isSupportSoftwareChargeLimit(); 76 | }, []); 77 | 78 | const showPowerTab = useMemo(() => { 79 | return supportChargeLimit || isSupportSoftwareChargeLimit; 80 | }, [supportChargeLimit, isSupportSoftwareChargeLimit]); 81 | 82 | return ( 83 | <> 84 | 93 | 94 | {show && ( 95 |
105 | { 108 | updateCurrentTabRoute(tabID); 109 | }} 110 | tabs={[ 111 | { 112 | title: , 113 | content: , 114 | id: "cpu", 115 | }, 116 | { 117 | title: ( 118 | 119 | ), 120 | content: , 121 | id: "gpu", 122 | }, 123 | { 124 | title: , 125 | content: , 126 | id: "fans", 127 | }, 128 | ...(showPowerTab 129 | ? [ 130 | { 131 | title: ( 132 | 136 | ), 137 | content: , 138 | id: "power", 139 | }, 140 | ] 141 | : []), 142 | { 143 | title: , 144 | content: , 145 | id: "more", 146 | }, 147 | ]} 148 | /> 149 |
150 | )} 151 | {!show && } 152 | 153 | ); 154 | }; 155 | const Content: FC<{}> = ({}) => { 156 | const [useOldUI, setUseOldUI] = useState(Settings.useOldUI); 157 | const [show, setShow] = useState(Settings.ensureEnable()); 158 | 159 | const hide = (ishide: boolean) => { 160 | setShow(!ishide); 161 | }; 162 | 163 | const refresh = () => { 164 | setUseOldUI(Settings.useOldUI); 165 | }; 166 | 167 | //listen Settings 168 | useEffect(() => { 169 | PluginManager.listenUpdateComponent( 170 | ComponentName.TAB_ALL, 171 | [ComponentName.TAB_ALL], 172 | (_ComponentName, updateType) => { 173 | switch (updateType) { 174 | case UpdateType.HIDE: { 175 | hide(true); 176 | break; 177 | } 178 | case UpdateType.SHOW: { 179 | hide(false); 180 | break; 181 | } 182 | case UpdateType.UPDATE: { 183 | refresh(); 184 | break; 185 | } 186 | } 187 | } 188 | ); 189 | }, []); 190 | 191 | return ( 192 | <> 193 | {PluginManager.isIniting() && ( 194 | 195 | 196 | 197 | )} 198 | {PluginManager.isRunning() && 199 | (useOldUI ? : )} 200 | {PluginManager.isError() && ( 201 | <> 202 | 203 |
204 | {`Error: ${PluginManager.getErrorMessage()}`} 205 |
206 |
207 | 208 | 209 | )} 210 | 211 | ); 212 | }; 213 | 214 | export default definePlugin(() => { 215 | try { 216 | console.log(">>>>>>>>>>>>>>>> Registering plugin PowerControl"); 217 | PluginManager.register(); 218 | } catch (e) { 219 | console.log("Error while registering plugin", e); 220 | } 221 | 222 | return { 223 | title:
PowerControl
, 224 | titleView: , 225 | content: , 226 | icon: , 227 | onDismount() { 228 | PluginManager?.unregister(); 229 | }, 230 | }; 231 | }); 232 | -------------------------------------------------------------------------------- /src/tab/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./tabCpu"; 2 | export * from "./tabGpu"; 3 | export * from "./tabPower"; 4 | export * from "./tabMore"; 5 | export * from "./tabFans"; -------------------------------------------------------------------------------- /src/tab/tabCpu.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { CPUComponent } from "../components"; 3 | 4 | export const TabCpu: FC = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/tab/tabFans.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { FANComponent } from "../components/fan"; 3 | 4 | export const TabFans: FC = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/tab/tabGpu.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { GPUComponent } from "../components"; 3 | 4 | export const TabGpu: FC = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/tab/tabMore.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { MoreComponent } from "../components"; 3 | 4 | export const TabMore: FC = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/tab/tabPower.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { PowerComponent } from "../components"; 3 | 4 | export const TabPower: FC = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | declare global { 4 | namespace JSX { 5 | interface IntrinsicElements { 6 | [elemName: string]: any; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/util/enum.ts: -------------------------------------------------------------------------------- 1 | export enum GPUMODE { 2 | NATIVE = "NATIVE", //系统原生设置 3 | NOLIMIT = "NOLIMIT", //不限制 4 | FIX = "FIX", //固定频率 5 | RANGE = "RANGE", //系统调度 6 | AUTO = "AUTO", //自动频率 7 | } 8 | export enum FANMODE { 9 | NOCONTROL = 0, //不控制 10 | FIX = 1, //固定 11 | CURVE = 2, //曲线 12 | } 13 | 14 | export enum FAN_PWM_MODE { 15 | SINGLE = 0, // 单文件 16 | MULTI_SAME = 1, // 多文件相同值 17 | MULTI_DIFF = 2, // 多文件不同值 18 | } 19 | 20 | export enum FANPROFILEACTION { 21 | DELETE = "DELETE", //删除风扇配置 22 | USE = "USE", //使用风扇配置 23 | EDIT = "EDIT", //编辑风扇配置 24 | ADD = "ADD", //添加风扇配置 25 | CANCEL = "CANCEL", //取消当前配置 26 | } 27 | 28 | export enum APPLYTYPE { 29 | SET_ALL = "ALL", 30 | SET_CPUBOOST = "SET_CPUBOOST", 31 | SET_CPUCORE = "SET_CPUCORE", 32 | SET_TDP = "SET_TDP", 33 | SET_CPU_MAX_PERF = "SET_CPU_MAX_PERF", 34 | SET_GPUMODE = "SET_GPUMODE", 35 | SET_FAN_ALL = "SET_FAN_ALL", 36 | SET_FANMODE = "SET_FANMODE", 37 | SET_FANRPM = "SET_FANRPM", 38 | SET_GPUSLIDERFIX = "SET_GPUSLIDEFIX", 39 | SET_CPU_GOVERNOR = "SET_CPU_GOVERNOR", 40 | SET_EPP = "SET_EPP", 41 | SET_POWER_BATTERY = "SET_POWER_BATTERY", 42 | } 43 | 44 | export enum ComponentName { 45 | SET_ENABLE = "SET_ENABLE", 46 | SET_PERAPP = "SET_PERAPP", 47 | CPU_ALL = "CPU_ALL", 48 | CPU_BOOST = "CPU_BOOST", 49 | CPU_SMT = "CPU_SMT", 50 | CPU_NUM = "CPU_NUM", 51 | CPU_TDP = "CPU_TDP", 52 | CPU_PERFORMANCE = "CPU_PERFORMANCE", 53 | CPU_GOVERNOR = "CPU_GOVERNOR", 54 | CPU_EPP = "CPU_EPP", 55 | EPP_LEVEL_1 = "EPP_LEVEL_1", 56 | EPP_LEVEL_2 = "EPP_LEVEL_2", 57 | EPP_LEVEL_3 = "EPP_LEVEL_3", 58 | GPU_ALL = "GPU_ALL", 59 | GPU_FREQMODE = "GPU_FREQMODE", 60 | GPU_FREQFIX = "GPU_FREQFIX", 61 | GPU_FREQRANGE = "GPU_FREQRANGE", 62 | GPU_FREQAUTO = "GPU_FREQAUTO", 63 | GPU_SLIDERFIX = "GPU_SLIDERFIX", 64 | FAN_ALL = "FAN_ALL", 65 | FAN_RPM = "FAN_RPM", 66 | FAN_DISPLAY = "FAN_DISPLAY", 67 | SET_PERACMODE = "SET_PERACMODE", 68 | CUSTOM_TDP = "CUSTOM_TDP", 69 | POWER_ALL = "POWER_ALL", 70 | POWER_BYPASS_CHARGE = "POWER_BYPASS_CHARGE", 71 | POWER_CHARGE_LIMIT = "POWER_CHARGE_LIMIT", 72 | TAB_ALL = "TAB_ALL", 73 | } 74 | 75 | export enum UpdateType { 76 | DISABLE = "DISABLE", 77 | UPDATE = "UPDATE", 78 | HIDE = "HIDE", 79 | SHOW = "SHOW", 80 | ENABLE = "ENABLE", 81 | DISMOUNT = "DISMOUNT", 82 | } 83 | 84 | export enum PluginState { 85 | INIT = "0", 86 | RUN = "1", 87 | QUIT = "2", 88 | ERROR = "3", 89 | } 90 | 91 | export enum Patch { 92 | TDPPatch = "TDPPatch", 93 | GPUPerformancePatch = "GPUPerformancePatch", 94 | } 95 | 96 | export enum GPUPerformanceLevel { 97 | DISABLE = 1, 98 | ENABLE = 2, 99 | } 100 | 101 | export enum SettingChangeEvent { 102 | GPUMODE = "GPUMODE", 103 | } 104 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./backend" 2 | export * from "./pluginMain" 3 | export * from "./settings" 4 | export * from "./position" 5 | export * from "./steamClient" 6 | export * from "./enum" 7 | export * from "./patch" 8 | export * from "./steamUtils" 9 | export * from "./version" 10 | export * from "./logger" 11 | export * from "./timeout" -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { Backend } from "."; 2 | 3 | /** 4 | * Logger utility class for consistent logging across the application 5 | */ 6 | export class Logger { 7 | private static getTimestamp(): string { 8 | const now = new Date(); 9 | const year = now.getFullYear(); 10 | const month = String(now.getMonth() + 1).padStart(2, '0'); 11 | const day = String(now.getDate()).padStart(2, '0'); 12 | const hours = String(now.getHours()).padStart(2, '0'); 13 | const minutes = String(now.getMinutes()).padStart(2, '0'); 14 | const seconds = String(now.getSeconds()).padStart(2, '0'); 15 | const milliseconds = String(now.getMilliseconds()).padStart(3, '0'); 16 | return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; 17 | } 18 | 19 | private static formatMessage(level: string, message: string): string { 20 | const timestamp = this.getTimestamp(); 21 | return `[${timestamp}] ${level} ${message}`; 22 | } 23 | 24 | static info(message: string): void { 25 | const formattedMessage = this.formatMessage('INFO', message); 26 | console.log(formattedMessage); 27 | Backend.logInfo(formattedMessage); 28 | } 29 | 30 | static error(message: string): void { 31 | const formattedMessage = this.formatMessage('ERROR', message); 32 | console.error(formattedMessage); 33 | Backend.logError(formattedMessage); 34 | } 35 | 36 | static warn(message: string): void { 37 | const formattedMessage = this.formatMessage('WARN', message); 38 | console.warn(formattedMessage); 39 | Backend.logWarn(formattedMessage); 40 | } 41 | 42 | static debug(message: string): void { 43 | const formattedMessage = this.formatMessage('DEBUG', message); 44 | console.debug(formattedMessage); 45 | Backend.logDebug(formattedMessage); 46 | } 47 | } -------------------------------------------------------------------------------- /src/util/position.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject, JsonProperty } from "typescript-json-serializer"; 2 | 3 | @JsonObject() 4 | export class FanPosition { 5 | @JsonProperty() 6 | temperature?: number; 7 | @JsonProperty() 8 | fanRPMpercent?: number; 9 | 10 | static tempMax: number = 100; 11 | static fanMax: number = 100; 12 | static fanMin: number = 0; 13 | static tempMin: number = 0; 14 | 15 | constructor(temperature: number, fanRPMpercent: number) { 16 | this.fanRPMpercent = Math.min( 17 | Math.max(fanRPMpercent, FanPosition.fanMin), 18 | FanPosition.fanMax 19 | ); 20 | this.temperature = Math.min( 21 | Math.max(temperature, FanPosition.tempMin), 22 | FanPosition.tempMax 23 | ); 24 | } 25 | 26 | public getCanvasPos(canWidth: number, canHeight: number) { 27 | var canPosx = Math.min( 28 | Math.max((this.temperature!! / FanPosition.tempMax) * canWidth, 0), 29 | canWidth 30 | ); 31 | var canPosy = Math.min( 32 | Math.max((1 - this.fanRPMpercent!! / FanPosition.fanMax) * canHeight, 0), 33 | canHeight 34 | ); 35 | return [canPosx, canPosy]; 36 | } 37 | 38 | public isCloseToOther(other: FanPosition, distance: number) { 39 | var getDis = Math.sqrt( 40 | Math.pow(other.temperature!! - this.temperature!!, 2) + 41 | Math.pow(other.fanRPMpercent!! - this.fanRPMpercent!!, 2) 42 | ); 43 | return getDis <= distance; 44 | } 45 | public static createFanPosByCanPos( 46 | canx: number, 47 | cany: number, 48 | canWidth: number, 49 | canHeight: number 50 | ) { 51 | var temperature = Math.min( 52 | Math.max((canx!! / canWidth) * this.tempMax, this.tempMin), 53 | this.tempMax 54 | ); 55 | var fanRPMpercent = Math.min( 56 | Math.max((1 - cany!! / canHeight) * this.fanMax, this.fanMin), 57 | this.fanMax 58 | ); 59 | return new FanPosition(temperature, fanRPMpercent); 60 | } 61 | } 62 | /* 63 | export class canvasPosition { 64 | @JsonProperty() 65 | canx?:number; 66 | @JsonProperty() 67 | cany?:number; 68 | constructor(canx:number,cany:number){ 69 | this.canx=canx; 70 | this.cany=cany; 71 | } 72 | public getFanPos(canWidth:number,canHeight:number) 73 | { 74 | const tempMax=100; 75 | const fanMax=100; 76 | const fanMin=0; 77 | const tempMin=0; 78 | var temperature=Math.min(Math.max(this.canx!!/canWidth*tempMax,tempMin),tempMax); 79 | var fanRPMpercent=Math.min(Math.max((1-this.cany!!/canHeight)*fanMax,fanMin),fanMax); 80 | return new fanPosition(temperature,fanRPMpercent) 81 | } 82 | } 83 | */ 84 | //通过画布位置来调整文字位置 85 | export const getTextPosByCanvasPos = ( 86 | canPosx: number, 87 | canPosy: number, 88 | canWidth: number, 89 | _canHeight: number 90 | ) => { 91 | var textlen = 55; 92 | var textheight = 12; 93 | var offsetX = 0; 94 | var offsetY = 0; 95 | if (canPosx + textlen / 2 >= canWidth - 5) { 96 | offsetX = canWidth - textlen - canPosx; 97 | } else if (canPosx - textlen / 2 <= 5) { 98 | offsetX = -canPosx; 99 | } else { 100 | offsetX = -textlen / 2 + 2; 101 | } 102 | if (canPosy - textheight <= 5) { 103 | offsetY = textheight + 5; 104 | } else { 105 | offsetY = -textheight; 106 | } 107 | return [canPosx + offsetX, canPosy + offsetY]; 108 | }; 109 | 110 | export const calPointInLine = ( 111 | lineStart: FanPosition, 112 | lineEnd: FanPosition, 113 | calPointIndex: number 114 | ) => { 115 | if (lineStart.temperature!! > lineEnd.temperature!!) return null; 116 | if ( 117 | calPointIndex < lineStart.temperature!! || 118 | calPointIndex > lineEnd.temperature!! 119 | ) 120 | return null; 121 | var deltaY = lineEnd.fanRPMpercent!! - lineStart.fanRPMpercent!!; 122 | var deltaX = lineEnd.temperature!! - lineStart.temperature!!; 123 | var calPointY = 124 | deltaX == 0 125 | ? deltaY 126 | : (calPointIndex - lineStart.temperature!!) * (deltaY / deltaX) + 127 | lineStart.fanRPMpercent!!; 128 | return new FanPosition(calPointIndex, calPointY); 129 | }; 130 | -------------------------------------------------------------------------------- /src/util/steamUtils.tsx: -------------------------------------------------------------------------------- 1 | import { Module, findModuleChild } from "@decky/ui"; 2 | import { SystemInfo } from "."; 3 | import { ReactNode } from "react"; 4 | import { FaSuperpowers } from "react-icons/fa"; 5 | import { toaster } from "@decky/api"; 6 | 7 | //#region Find SteamOS modules 8 | const findModule = (property: string) => { 9 | return findModuleChild((m: Module) => { 10 | if (typeof m !== "object") return undefined; 11 | for (let prop in m) { 12 | try { 13 | if (m[prop][property]) return m[prop]; 14 | } catch { 15 | return undefined; 16 | } 17 | } 18 | }); 19 | }; 20 | // @ts-ignore 21 | const NavSoundMap = findModule("ToastMisc"); 22 | //#endregion 23 | 24 | export interface NotifyProps { 25 | message: ReactNode; 26 | title?: ReactNode; 27 | logo?: ReactNode; 28 | icon?: ReactNode; 29 | showToast?: boolean; 30 | playSound?: boolean; 31 | sound?: number; 32 | duration?: number; 33 | } 34 | 35 | export class SteamUtils { 36 | //#region Notification Wrapper 37 | static async notify({ 38 | title, 39 | message, 40 | logo, 41 | icon, 42 | showToast, 43 | playSound, 44 | sound, 45 | duration, 46 | }: NotifyProps) { 47 | let toastData = { 48 | title: title || "PowerControl", 49 | body: message, 50 | logo: logo, 51 | icon: icon || , 52 | duration: duration, 53 | sound: sound || NavSoundMap?.ToastMisc, 54 | playSound: playSound || false, 55 | showToast: showToast || false, 56 | }; 57 | toaster.toast(toastData); 58 | } 59 | 60 | static async simpleToast(message: string, duration?: number) { 61 | this.notify({ message, showToast: true, duration }); 62 | } 63 | ; 64 | static async getSystemInfo() : Promise { 65 | const systemInfo = await SteamClient.System.GetSystemInfo(); 66 | return systemInfo; 67 | } 68 | } -------------------------------------------------------------------------------- /src/util/timeout.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "./logger"; 2 | 3 | /** 4 | * 超时工具类 5 | */ 6 | export class Timeout { 7 | /** 8 | * 带超时的异步函数包装器 9 | * @param fn 要执行的异步函数 10 | * @param timeoutMs 超时时间(毫秒) 11 | * @param errorMessage 超时错误信息 12 | * @returns Promise 执行结果 13 | */ 14 | static async withTimeout( 15 | fn: () => Promise, 16 | timeoutMs: number, 17 | errorMessage: string = "Operation timed out" 18 | ): Promise { 19 | const timeoutPromise = new Promise((_, reject) => { 20 | setTimeout(() => { 21 | reject(new Error(`${errorMessage} (${timeoutMs}ms)`)); 22 | }, timeoutMs); 23 | }); 24 | 25 | try { 26 | return await Promise.race([fn(), timeoutPromise]); 27 | } catch (error) { 28 | if (error instanceof Error && error.message.includes("timed out")) { 29 | Logger.error(error.message); 30 | } 31 | throw error; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/util/version.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Compare two version strings 3 | * @param v1 First version string 4 | * @param v2 Second version string 5 | * @returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal 6 | */ 7 | export const compareVersions = (v1: string, v2: string): number => { 8 | const v1Parts = v1.replace("v", "").split(".").map(Number); 9 | const v2Parts = v2.replace("v", "").split(".").map(Number); 10 | 11 | for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { 12 | const v1Part = v1Parts[i] || 0; 13 | const v2Part = v2Parts[i] || 0; 14 | if (v1Part > v2Part) return 1; 15 | if (v1Part < v2Part) return -1; 16 | } 17 | return 0; 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "ESNext", 5 | "target": "ES2020", 6 | "jsx": "react", 7 | "jsxFactory": "window.SP_REACT.createElement", 8 | "jsxFragmentFactory": "window.SP_REACT.Fragment", 9 | "declaration": false, 10 | "moduleResolution": "node", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "esModuleInterop": true, 14 | "noImplicitReturns": true, 15 | "noImplicitThis": true, 16 | "noImplicitAny": true, 17 | "emitDecoratorMetadata": true, 18 | "experimentalDecorators": true, 19 | "strict": true, 20 | "allowSyntheticDefaultImports": true, 21 | "skipLibCheck": true, 22 | "resolveJsonModule":true 23 | }, 24 | "include": ["src"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------