Repository: TamtamHero/fw-fanctrl
Branch: main
Commit: 776f619cea2b
Files: 66
Total size: 104.6 KB
Directory structure:
gitextract_s4i5ocno/
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report---packaging-aur.md
│ │ ├── bug-report---packaging-nix.md
│ │ ├── bug-report---packaging-windows.md
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── other.md
│ └── workflows/
│ ├── auto-tag-and-release.yml
│ └── pr-check-formatting.yml
├── .gitignore
├── LICENSE
├── README.md
├── doc/
│ ├── README.md
│ ├── commands.md
│ ├── configuration.md
│ └── nixos.md
├── fetch/
│ └── ectool/
│ ├── LICENSE
│ └── linux/
│ ├── gitlab_job_id
│ └── hash.sha256
├── install.sh
├── post-install.sh
├── pre-uninstall.sh
├── pyproject.toml
├── services/
│ ├── fw-fanctrl.service
│ └── system-sleep/
│ └── fw-fanctrl-suspend
└── src/
└── fw_fanctrl/
├── CommandParser.py
├── Configuration.py
├── FanController.py
├── Strategy.py
├── __init__.py
├── __main__.py
├── _resources/
│ ├── config.json
│ └── config.schema.json
├── dto/
│ ├── Printable.py
│ ├── __init__.py
│ ├── command_result/
│ │ ├── CommandResult.py
│ │ ├── ConfigurationReloadCommandResult.py
│ │ ├── PrintActiveCommandResult.py
│ │ ├── PrintCurrentStrategyCommandResult.py
│ │ ├── PrintFanSpeedCommandResult.py
│ │ ├── PrintStrategyListCommandResult.py
│ │ ├── ServicePauseCommandResult.py
│ │ ├── ServiceResumeCommandResult.py
│ │ ├── SetConfigurationCommandResult.py
│ │ ├── StrategyChangeCommandResult.py
│ │ ├── StrategyResetCommandResult.py
│ │ └── __init__.py
│ └── runtime_result/
│ ├── RuntimeResult.py
│ ├── StatusRuntimeResult.py
│ └── __init__.py
├── enum/
│ ├── CommandStatus.py
│ ├── OutputFormat.py
│ └── __init__.py
├── exception/
│ ├── ConfigurationParsingException.py
│ ├── InvalidStrategyException.py
│ ├── SocketAlreadyRunningException.py
│ ├── SocketCallException.py
│ ├── UnimplementedException.py
│ ├── UnknownCommandException.py
│ └── __init__.py
├── hardwareController/
│ ├── EctoolHardwareController.py
│ ├── HardwareController.py
│ └── __init__.py
└── socketController/
├── SocketController.py
├── UnixSocketController.py
└── __init__.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = space
indent_size = 4
max_line_length = 120
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
end_of_line = lf
[.github/workflows/**/*]
indent_size = 2
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report---packaging-aur.md
================================================
---
name: Bug report | packaging/AUR
about: Report a bug involving the AUR packaging
title: "[BUG] [packaging/AUR]"
labels: bug, packaging/AUR
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Error message**
```txt
If applicable, add the full error message.
```
**Environment (please complete the following information):**
- OS: [e.g. Arch]
- Version [e.g. commit 176d34b]
- Configuration file [config.json]
- Installation method [full command)/package manager/other...]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report---packaging-nix.md
================================================
---
name: Bug report | packaging/nix
about: Report a bug involving the nix packaging
title: "[BUG] [packaging/nix]"
labels: bug, packaging/nix
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Error message**
```txt
If applicable, add the full error message.
```
**Environment (please complete the following information):**
- OS: [e.g. Arch]
- Version [e.g. commit 176d34b]
- Configuration file [config.json]
- Installation method [full command)/package manager/other...]
**Additional context**
Add any other context about the problem here.
**Assigned maintainers**
- @Svenum
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report---packaging-windows.md
================================================
---
name: Bug report | packaging/windows
about: Report a bug involving the windows packaging
title: "[BUG] [packaging/windows]"
labels: bug, packaging/windows
assignees: 'leopoldhub'
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Error message**
```txt
If applicable, add the full error message.
```
**Environment (please complete the following information):**
- OS: [e.g. Windows 11]
- Version [e.g. commit 176d34b]
- Configuration file [config.json]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Report a bug in the program
title: "[BUG] "
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Error message**
```txt
If applicable, add the full error message.
```
**Environment (please complete the following information):**
- OS: [e.g. Arch]
- Version [e.g. commit 176d34b]
- Configuration file [config.json]
- Installation method [full command)/package manager/other...]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE]"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
**Would you like to be involved in the development?**
[yes/no]
================================================
FILE: .github/ISSUE_TEMPLATE/other.md
================================================
---
name: Other
about: Any other issue/question
title: ''
labels: ''
assignees: ''
---
================================================
FILE: .github/workflows/auto-tag-and-release.yml
================================================
name: Auto Tag and Release on Version Change
on:
push:
branches:
- main
jobs:
tag:
name: Tag if Version Changed
runs-on: ubuntu-latest
outputs:
current_version: ${{ steps.extract.outputs.current }}
version_changed: ${{ steps.extract.outputs.version_changed }}
steps:
- name: Checkout repository with history
uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies extraction dependencies
run: |
python -m pip install --upgrade pip toml
- name: Extract versions and determine change
id: extract
run: |
PREVIOUS_VERSION=$(git show HEAD^:pyproject.toml | python -c "import sys, toml; print(toml.loads(sys.stdin.read())['project']['version'])")
CURRENT_VERSION=$(cat pyproject.toml | python -c "import sys, toml; print(toml.loads(sys.stdin.read())['project']['version'])")
echo "previous=$PREVIOUS_VERSION" >> $GITHUB_OUTPUT
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
if [ "$PREVIOUS_VERSION" != "$CURRENT_VERSION" ]; then
echo "version_changed=true" >> $GITHUB_OUTPUT
else
echo "version_changed=false" >> $GITHUB_OUTPUT
fi
- name: Create git tag if version changed
if: ${{ steps.extract.outputs.version_changed == 'true' }}
run: |
git config user.name "${{ github.actor }}"
git config user.email "${{ github.actor }}@users.noreply.github.com"
git tag "v${{ steps.extract.outputs.current }}"
git push origin "v${{ steps.extract.outputs.current }}"
release:
name: Release with New Tag
runs-on: ubuntu-latest
needs: tag
if: ${{ needs.tag.outputs.version_changed == 'true' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip build
- name: Build distribution packages
run: |
python -m build -s
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: 'dist/*'
tag_name: 'v${{needs.tag.outputs.current_version}}'
================================================
FILE: .github/workflows/pr-check-formatting.yml
================================================
name: Check Formatting on PR
on:
pull_request:
types:
- synchronize
branches:
- main
jobs:
check:
name: Check Formatting
runs-on: ubuntu-latest
steps:
- name: Checkout shallow repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install -e ".[dev]"
- name: Check formatting
run: |
black --check --diff .
================================================
FILE: .gitignore
================================================
### Python template
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Project ignores
.idea/
.temp/
================================================
FILE: LICENSE
================================================
BSD 3-Clause License
Copyright (c) 2022, TamTamHero
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
# fw-fanctrl
[](https://github.com/TamtamHero/fw-fanctrl/tree/main)

[](https://www.python.org/downloads)
## Platforms
[](https://github.com/TamtamHero/fw-fanctrl/tree/main)
[](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix/doc/nix-flake.md)
**Third-party**<br>
[](https://aur.archlinux.org/packages/fw-fanctrl-git)
[](https://github.com/tulilirockz/fw-fanctrl-rpm)
_You are a package manager? Add your platform here!_
## Description
Fw-fanctrl is a simple Python CLI service that controls Framework Laptop's fan(s)
speed according to a configurable speed/temperature curve.
Its default strategy aims for very quiet fan operation, but you can choose amongst the other provided strategies, or
easily configure your own for a different comfort/performance trade-off.
It also is possible to assign separate strategies depending on whether the laptop is charging or discharging.
Under the hood, it uses [ectool](https://gitlab.howett.net/DHowett/ectool)
to change parameters in Framework's embedded controller (EC).
It is compatible with all 13" and 16" models, both AMD/Intel CPUs, with or without a discrete GPU.
If the service is paused or stopped, the fans will revert to their default behaviour.
## Table of Content
<!-- TOC -->
* [fw-fanctrl](#fw-fanctrl)
* [Platforms](#platforms)
* [Description](#description)
* [Table of Content](#table-of-content)
* [Third-party projects](#third-party-projects)
* [Documentation](#documentation)
* [Installation](#installation)
* [Platforms](#platforms-1)
* [Requirements](#requirements)
* [Dependencies](#dependencies)
* [Instructions](#instructions)
* [Update](#update)
* [Uninstall](#uninstall)
* [Development Setup](#development-setup)
<!-- TOC -->
## Third-party projects
_Have some cool project to show? Add yours to the list!_
| Name | Description | Picture |
|-------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [fw‑fanctrl‑gui](https://github.com/leopoldhub/fw-fanctrl-gui) | Simple customtkinter python gui with system tray for fw‑fanctrl | [<img src="https://github.com/leopoldhub/fw-fanctrl-gui/blob/master/doc/screenshots/tray.png?raw=true" width="200">](https://github.com/leopoldhub/fw-fanctrl-gui) |
| [fw-fanctrl-revived-gnome-shell-extension](https://github.com/ghostdevv/fw-fanctrl-revived-gnome-shell-extension) | A Gnome extension that provides a convenient way to control your framework laptop fan profile when using fw-fanctrl | [<img src="https://raw.githubusercontent.com/ghostdevv/fw-fanctrl-revived-gnome-shell-extension/refs/heads/main/.github/example.png" width="200">](https://github.com/ghostdevv/fw-fanctrl-revived-gnome-shell-extension) |
| [fw_fanctrl_applet](https://github.com/not-a-feature/fw_fanctrl_applet) | Cinnamon applet to control the framework fan-speed strategy using fw-fanctrl | [<img src="https://raw.githubusercontent.com/not-a-feature/fw_fanctrl_applet/main/screenshot.png" width="200">](https://github.com/not-a-feature/fw_fanctrl_applet) |
| [ulauncher-fw-fanctrl](https://github.com/ghostdevv/ulauncher-fw-fanctrl) | A fw-fanctrl extension for the app launcher [ulauncher](https://ulauncher.io) | [<img src="https://raw.githubusercontent.com/ghostdevv/ulauncher-fw-fanctrl/32f7c0484b8903daa85f1b963ed4e901d7379a8a/.github/demo.png" width="200">](https://github.com/ghostdevv/ulauncher-fw-fanctrl) |
## Documentation
More documentation could be found [here](./doc/README.md).
## Installation
### Platforms
| Name | Package | Branch | Documentation |
|------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| Linux / Global | [installation script](https://github.com/TamtamHero/fw-fanctrl/blob/main/install.sh) | [main](https://github.com/TamtamHero/fw-fanctrl/tree/main) | [instructions](https://github.com/TamtamHero/fw-fanctrl/tree/main?tab=readme-ov-file#instructions) |
| NixOS (<= 25.04) | [flake](https://github.com/TamtamHero/fw-fanctrl/blob/packaging/nix/flake.nix) | [packaging/nix](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix) | [packaging/nix/doc/nix‑flake](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix/doc/nix-flake.md) |
| NixOS (Unstable) | [derivation](https://search.nixos.org/packages?channel=unstable&show=fw-fanctrl&from=0&size=50&sort=relevance&type=packages&query=fw-fanctrl)||[packaging/nix/doc/nix‑flake](https://github.com/TamtamHero/fw-fanctrl/tree/main/doc/nixos.md)|
**Third-party**
| Name | Package | Documentation |
|------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| Arch Linux | [AUR](https://aur.archlinux.org/packages/fw-fanctrl-git) | |
| Fedora / RPM | [COPR](https://copr.fedorainfracloud.org/coprs/tulilirockz/fw-fanctrl/package/fw-fanctrl/) | [GIT repository](https://github.com/tulilirockz/fw-fanctrl-rpm) |
### Requirements
| Name | Version | Url |
|------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| Linux kernel | \>= 6.11.x | |
| Python | \>= 3.12.x | [https://www.python.org/downloads](https://www.python.org/downloads) |
### Dependencies
Dependencies are downloaded and installed automatically, but can be excluded from the installation script if you wish to
do this manually.
| Name | Version | Url | Sub‑dependencies | Exclusion argument |
|------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|------------------------|-------------------------|
| DHowett@ectool | build#899 | [https://gitlab.howett.net/DHowett/ectool](https://gitlab.howett.net/DHowett/ectool) | libftdi | `--no-ectool` |
### Instructions
[Download the repo](https://github.com/TamtamHero/fw-fanctrl/archive/refs/heads/main.zip) and extract it manually, or
download/clone it with the appropriate tools:
```shell
git clone "https://github.com/TamtamHero/fw-fanctrl.git"
```
```shell
curl -L "https://github.com/TamtamHero/fw-fanctrl/archive/refs/heads/main.zip" -o "./fw-fanctrl.zip" && unzip "./fw-fanctrl.zip" -d "./fw-fanctrl" && rm -rf "./fw-fanctrl.zip"
```
Then run the installation script with administrator privileges
> ⚠ **Linux Mint** users should add the `--effective-installation-dir "/usr/local/bin"` option.
>
> ⚠ **Fedora Atomic desktops** users should add the `--prefix-dir "/var/usrlocal/"` option.
```bash
sudo ./install.sh
```
You can add a number of arguments to the installation command to suit your needs
| argument | description |
|---------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| `--dest-dir <installation destination directory (defaults to /)>` | specify an installation destination directory |
| `--prefix-dir <installation prefix directory (defaults to /usr)>` | specify an installation prefix directory |
| `--sysconf-dir <system configuration destination directory (defaults to /etc)>` | specify a default configuration directory |
| `--no-ectool` | disable ectool installation and service activation |
| `--no-post-install` | disable post-install process |
| `--no-pre-uninstall` | disable pre-uninstall process |
| `--no-battery-sensors` | disable checking battery temperature sensors |
| `--no-pip-install` | disable the pip installation (should be done manually instead) |
| `--pipx` | install using pipx instead of pip (useful if os does not allow global pip install like debian ) |
| `--python-prefix-dir <python installation prefix directory (defaults to [dest-dir][prefix-dir])>` | specify the python prefix directory for package installation |
| `--effective-installation-dir <directory (defaults to [python-prefix-dir]/bin)>` | overrides the installation in which our `fw-fanctrl` executable is |
## Update
To update, you can download or pull the appropriate branch from this repository, and run the installation script again.
## Uninstall
To uninstall, run the installation script with the `--remove` argument, as well as other
corresponding [arguments if necessary](#instructions)
```bash
sudo ./install.sh --remove
```
## Development Setup
> It is recommended to use a virtual environment to install development dependencies
Install the development dependencies with the following command:
```shell
pip install -e ".[dev]"
```
The project uses the [black](https://github.com/psf/black) formatter.
Please format your contributions before commiting them.
```shell
python -m black .
```
================================================
FILE: doc/README.md
================================================
# Table of Content
- [Default Installation](../README.md#installation)
- [Development Setup](../README.md#development-setup)
- [NixOS Flake](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix/doc/nix-flake.md)
- [Commands](./commands.md)
- [Configuration](./configuration.md)
================================================
FILE: doc/commands.md
================================================
# Commands
Here is a list of commands and options used to interact with the service.
the base of all commands is the following
```shell
fw-fanctrl [commands and options]
```
First, the global options
| Option | Optional | Choices | Default | Description |
|---------------------------|----------|---------------|---------|--------------------------------------------------------------------------------|
| --socket-controller, --sc | yes | unix | unix | the socket controller to use for communication between the cli and the service |
| --output-format | yes | NATURAL, JSON | NATURAL | the client socket controller output format |
**run**
run the service manually
If you have installed it correctly, the systemd `fw-fanctrl.service` service will do this for you, so you probably will
never need those.
| Option | Optional | Choices | Default | Description |
|-----------------------------|----------|----------------|----------------------|-----------------------------------------------------------------------------------|
| \<strategy> | yes | | the default strategy | the name of the strategy to use |
| --config | yes | \[CONFIG_PATH] | | the configuration file path |
| --silent, -s | yes | | | disable printing speed/temp status to stdout |
| --hardware-controller, --hc | yes | ectool | ectool | the hardware controller to use for fetching and setting the temp and fan(s) speed |
| --no-battery-sensors | yes | | | disable checking battery temperature sensors (for mainboards without batteries) |
**use**
change the current strategy
| Option | Optional | Description |
|-------------|----------|---------------------------------|
| \<strategy> | no | the name of the strategy to use |
**reset**
reset to the default strategy
**reload**
reload the configuration file
**pause**
pause the service
**resume**
resume the service
**print**
print the selected information
| Option | Optional | Choices | Default | Description |
|--------------------|----------|---------------------------|---------|------------------------|
| \<print_selection> | yes | all, current, list, speed | all | what should be printed |
| Choice | Description |
|---------|----------------------------------|
| all | All details |
| current | The current strategy being used |
| list | List available strategies |
| speed | The current fan speed percentage |
================================================
FILE: doc/configuration.md
================================================
# Table of Content
<!-- TOC -->
* [Table of Content](#table-of-content)
* [Configuration](#configuration)
* [Default Strategy](#default-strategy)
* [Discharging Strategy](#discharging-strategy)
* [Strategies](#strategies)
* [Requirements](#requirements)
* [Speed Curve](#speed-curve)
* [Fan Speed Update Frequency](#fan-speed-update-frequency)
* [Moving Average Interval](#moving-average-interval)
<!-- TOC -->
# Configuration
The service uses these configuration files by default:
- Main configuration: `/etc/fw-fanctrl/config.json`
- JSON Schema: `/etc/fw-fanctrl/config.schema.json`
For custom installations using dest-dir or sysconf-dir parameters:
- `[dest-dir(/)][sysconf-dir(/etc)]/fw-fanctrl/config.json`
- `[dest-dir(/)][sysconf-dir(/etc)]/fw-fanctrl/config.schema.json`
The configuration contains a list of strategies, ranked from the quietest to loudest,
as well as the default and discharging strategies.
For example, one could use a lower fan speed strategy on discharging to optimize battery life (- noise, + heat),
and a high fan speed strategy on AC (+ noise, - heat).
**The schema contains the structure and restrictions for a valid configuration.**
You can add or edit strategies, and if you think you have one that deserves to be shared,
feel free to share it in [#110](https://github.com/TamtamHero/fw-fanctrl/issues/110).
## Default Strategy
The default strategy serves as the initial fan control profile when the service starts.
It remains active unless you manually select a different strategy,
at which point your chosen strategy takes precedence until you reset to the default strategy explicitly,
or the service restarts.
It can be changed by replacing the value of the `defaultStrategy` field with one of the strategies present in the
configuration.
e.g.:
```
"defaultStrategy": "medium"
```
## Discharging Strategy
The discharging strategy will be used when on default strategy behavior and battery power.
It can be changed by replacing the value of the `strategyOnDischarging` field with one of the strategies present in the
configuration.
```
"strategyOnDischarging": "laziest"
```
This field is optional and can be left empty for it to have the same behavior as on AC.
## Strategies
Define strategies under strategies object using this format:
```
"strategies": {
"strategy-name": {
"speedCurve": [ ... ],
"fanSpeedUpdateFrequency": 5,
"movingAverageInterval": 20
}
}
```
### Requirements
Strategies must have unique names composed of upper/lower case letters, numbers, underscores or hyphens.
`[a-zA-Z0-9_-]+`
And, at least have the `speedCurve` property defined.
### Speed Curve
It represents by the curve points for `f(temperature) = fan(s) speed`.
The `temp` field value is a number with precision of up to 0.01°C (e.g., 15.23),
while the `speed` is a positive integer between 0 and 100 %.
It should contain at least a single temperature point.
```
"speedCurve": [
{ "temp": 40, "speed": 20 },
{ "temp": 60.5, "speed": 50 },
{ "temp": 80.7, "speed": 100 }
]
```
> `fw-fanctrl` measures the CPU temperature, calculates a moving average of it, and then finds an
> appropriate `fan speed` value by interpolating on the curve.
### Fan Speed Update Frequency
It is the interval between fan speed adjustments.
- Lower values → faster response to temperature changes
- Higher values → smoother transitions
It is an optional positive integer comprised between 1 and 10 and defaults to 5.
```
"fanSpeedUpdateFrequency": 5
```
> This is for comfort, otherwise the speed will change too often, which is noticeable and annoying, especially at low
> speed.
### Moving Average Interval
It is the time window in seconds over which the moving average of temperature is calculated.
- Lower values → immediate reaction to spikes
- Higher values → stabilized readings
It is an optional positive integer comprised between 1 and 100 and defaults to 20.
```
"movingAverageInterval": 20
```
---
Once the configuration has been changed, you must reload it with the following command
```bash
fw-fanctrl reload
```
================================================
FILE: doc/nixos.md
================================================
# Module and Package
For [NixOS](https://nixos.org/) verion >25.05 this package (derivation) is in the offical [Nixpkgs](https://github.com/NixOS/nixpkgs/).
In addition we created a module to configure it via nix.
NixOS Search:
- [Module](https://search.nixos.org/options?channel=unstable&show=hardware.fw-fanctrl.enable&from=0&size=50&sort=relevance&type=packages&query=fw-fanctrl)
- [Package](https://search.nixos.org/packages?channel=unstable&show=fw-fanctrl&from=0&size=50&sort=relevance&type=packages&query=fw-fanctrl)
# Installation
Here is an example how you could configure `fw-fanctrl`:
```nix
hardware.fw-fanctrl = {
enable = true; # This is needed to enable the service
config = { # This option is only needed if you want to add additional strategies
defaultStrategy = "school";
strategyOnDischarging = "laziest"; # Must not be set
strategies = {
"school" = {
fanSpeedUpdateFrequency = 5;
movingAverageInterval = 40;
speedCurve = [
{ temp = 45; speed = 0; }
{ temp = 65; speed = 15; }
{ temp = 70; speed = 25; }
{ temp = 85; speed = 35; }
];
};
"lazy" = {
fanSpeedUpdateFrequency = 5;
movingAverageInterval = 30;
speedCurve = [
{ temp = 0; speed = 15; }
{ temp = 50; speed = 15; }
{ temp = 65; speed = 25; }
{ temp = 70; speed = 35; }
{ temp = 75; speed = 50; }
{ temp = 85; speed = 100; }
];
};
};
};
disableBatteryTempCheck = false;
};
```
This strategies gets appended to the [default config](https://github.com/TamtamHero/fw-fanctrl/blob/main/src/fw_fanctrl/_resources/config.json).
If you find any issue feel free to open an [Bug report | packaging/nix](https://github.com/TamtamHero/fw-fanctrl/issues)!
================================================
FILE: fetch/ectool/LICENSE
================================================
// Copyright 2010 The Chromium OS Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: fetch/ectool/linux/gitlab_job_id
================================================
899
================================================
FILE: fetch/ectool/linux/hash.sha256
================================================
ab94a1e9a33f592d5482dbfd4f42ad351ef91227ee3b3707333c0107d7f2b1b0
================================================
FILE: install.sh
================================================
#!/bin/bash
set -e
# Argument parsing
SHORT=r,d:,p:,s:,h
LONG=remove,dest-dir:,prefix-dir:,sysconf-dir:,no-ectool,no-pre-uninstall,no-post-install,no-battery-sensors,no-sudo,no-pip-install,pipx,python-prefix-dir:,effective-installation-dir:,help
VALID_ARGS=$(getopt -a --options $SHORT --longoptions $LONG -- "$@")
if [[ $? -ne 0 ]]; then
exit 1;
fi
TEMP_FOLDER='./.temp'
trap 'rm -rf $TEMP_FOLDER' EXIT
PREFIX_DIR="/usr"
DEST_DIR=""
SYSCONF_DIR="/etc"
SHOULD_INSTALL_ECTOOL=true
SHOULD_PRE_UNINSTALL=true
SHOULD_POST_INSTALL=true
SHOULD_REMOVE=false
NO_BATTERY_SENSOR=false
NO_SUDO=false
NO_PIP_INSTALL=false
PIPX=false
PYTHON_PREFIX_DIRECTORY_OVERRIDE=
EFFECTIVE_INSTALLATION_DIRECTORY_OVERRIDE=
eval set -- "$VALID_ARGS"
while true; do
case "$1" in
'--remove' | '-r')
SHOULD_REMOVE=true
;;
'--prefix-dir' | '-p')
PREFIX_DIR=$2
shift
;;
'--dest-dir' | '-d')
DEST_DIR=$2
shift
;;
'--sysconf-dir' | '-s')
SYSCONF_DIR=$2
shift
;;
'--no-ectool')
SHOULD_INSTALL_ECTOOL=false
;;
'--no-pre-uninstall')
SHOULD_PRE_UNINSTALL=false
;;
'--no-post-install')
SHOULD_POST_INSTALL=false
;;
'--no-battery-sensors')
NO_BATTERY_SENSOR=true
;;
'--no-sudo')
NO_SUDO=true
;;
'--no-pip-install')
NO_PIP_INSTALL=true
;;
'--pipx')
PIPX=true
;;
'--python-prefix-dir')
PYTHON_PREFIX_DIRECTORY_OVERRIDE=$2
shift
;;
'--effective-installation-dir')
EFFECTIVE_INSTALLATION_DIRECTORY_OVERRIDE=$2
shift
;;
'--help' | '-h')
echo "Usage: $0 [--remove,-r] [--dest-dir,-d <installation destination directory (defaults to $DEST_DIR)>] [--prefix-dir,-p <installation prefix directory (defaults to $PREFIX_DIR)>] [--sysconf-dir,-s system configuration destination directory (defaults to $SYSCONF_DIR)] [--no-ectool] [--no-post-install] [--no-pre-uninstall] [--no-sudo] [--no-pip-install] [--pipx] [--python-prefix-dir (defaults to $DEST_DIR$PREFIX_DIR)]" 1>&2
exit 0
;;
--)
break
;;
esac
shift
done
PYTHON_PREFIX_DIRECTORY="$DEST_DIR$PREFIX_DIR"
if [ -n "$PYTHON_PREFIX_DIRECTORY_OVERRIDE" ]; then
PYTHON_PREFIX_DIRECTORY=$PYTHON_PREFIX_DIRECTORY_OVERRIDE
fi
INSTALLATION_DIRECTORY="$PYTHON_PREFIX_DIRECTORY/bin"
if [ -n "$EFFECTIVE_INSTALLATION_DIRECTORY_OVERRIDE" ]; then
INSTALLATION_DIRECTORY=$EFFECTIVE_INSTALLATION_DIRECTORY_OVERRIDE
fi
PYTHON_SCRIPT_INSTALLATION_PATH="$INSTALLATION_DIRECTORY/fw-fanctrl"
if ! python3 -h 1>/dev/null 2>&1; then
echo "Missing package 'python3'!"
exit 1
fi
if [ "$NO_PIP_INSTALL" = false ]; then
if ! python3 -m pip -h 1>/dev/null 2>&1; then
echo "Missing python package 'pip'!"
exit 1
fi
fi
if [ "$PIPX" = true ]; then
if ! pipx -h >/dev/null 2>&1; then
echo "Missing package 'pipx'!"
exit 1
fi
fi
if [ "$SHOULD_REMOVE" = false ]; then
if ! python3 -m build -h 1>/dev/null 2>&1; then
echo "Missing python package 'build'!"
exit 1
fi
fi
# Root check
if [ "$EUID" -ne 0 ] && [ "$NO_SUDO" = false ]
then echo "This program requires root permissions or use the '--no-sudo' option"
exit 1
fi
SERVICES_DIR="./services"
SERVICE_EXTENSION=".service"
SERVICES="$(cd "$SERVICES_DIR" && find . -maxdepth 1 -maxdepth 1 -type f -name "*$SERVICE_EXTENSION" -exec basename {} "$SERVICE_EXTENSION" \;)"
SERVICES_SUBCONFIGS="$(cd "$SERVICES_DIR" && find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)"
function sanitizePath() {
local SANITIZED_PATH="$1"
local SANITIZED_PATH=${SANITIZED_PATH//..\//}
local SANITIZED_PATH=${SANITIZED_PATH#./}
local SANITIZED_PATH=${SANITIZED_PATH#/}
echo "$SANITIZED_PATH"
}
function build() {
echo "building package"
remove_target "dist/"
python3 -m build -s
find . -type d -name "*.egg-info" -exec rm -rf {} + 2> "/dev/null" || true
}
# safe remove function
function remove_target() {
local target="$1"
if [ -e "$target" ] || [ -L "$target" ]; then
if ! rm -rf "$target" 2> "/dev/null"; then
echo "Failed to remove: $target"
echo "Please run:"
echo " sudo ./install.sh --remove"
exit 1
fi
fi
}
# remove remaining legacy files
function uninstall_legacy() {
echo "removing legacy files"
remove_target "/usr/local/bin/fw-fanctrl"
remove_target "/usr/local/bin/ectool"
remove_target "/usr/local/bin/fanctrl.py"
remove_target "/etc/systemd/system/fw-fanctrl.service"
remove_target "$DEST_DIR$PREFIX_DIR/bin/fw-fanctrl"
}
function uninstall() {
if [ "$SHOULD_PRE_UNINSTALL" = true ]; then
if ! ./pre-uninstall.sh "$([ "$NO_SUDO" = true ] && echo "--no-sudo")"; then
echo "Failed to run ./pre-uninstall.sh. Run the script with root permissions,"
echo "or skip this step by using the --no-pre-uninstall option."
exit 1
fi
fi
# remove program services based on the services present in the './services' folder
echo "removing services"
for SERVICE in $SERVICES ; do
SERVICE=$(sanitizePath "$SERVICE")
# be EXTRA CAREFUL about the validity of the paths (dont wanna delete something important, right?... O_O)
remove_target "$DEST_DIR$PREFIX_DIR/lib/systemd/system/$SERVICE$SERVICE_EXTENSION"
done
# remove program services sub-configurations based on the sub-configurations present in the './services' folder
echo "removing services sub-configurations"
for SERVICE in $SERVICES_SUBCONFIGS ; do
SERVICE=$(sanitizePath "$SERVICE")
echo "removing sub-configurations for [$SERVICE]"
SUBCONFIGS="$(cd "$SERVICES_DIR/$SERVICE" && find . -mindepth 1 -type f)"
for SUBCONFIG in $SUBCONFIGS ; do
SUBCONFIG=$(sanitizePath "$SUBCONFIG")
echo "removing '$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG'"
remove_target "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG"
done
done
if [ "$NO_PIP_INSTALL" = false ]; then
echo "uninstalling python package"
if [ "$PIPX" = false ]; then
python3 -m pip uninstall -y fw-fanctrl 2> "/dev/null" || true
else
pipx --global uinistall fw-fanctrl 2> "/dev/null" || true
fi
fi
ectool autofanctrl 2> "/dev/null" || true # restore default fan manager
if [ "$SHOULD_INSTALL_ECTOOL" = true ]; then
remove_target "$DEST_DIR$PREFIX_DIR/bin/ectool"
fi
remove_target "$DEST_DIR$SYSCONF_DIR/fw-fanctrl"
remove_target "/run/fw-fanctrl"
uninstall_legacy
}
function install() {
uninstall_legacy
remove_target "$TEMP_FOLDER"
mkdir -p "$DEST_DIR$PREFIX_DIR/bin"
if [ "$SHOULD_INSTALL_ECTOOL" = true ]; then
mkdir "$TEMP_FOLDER"
installEctool "$TEMP_FOLDER" || (echo "an error occurred when installing ectool." && echo "please check your internet connection or consider installing it manually and using --no-ectool on the installation script." && exit 1)
remove_target "$TEMP_FOLDER"
fi
mkdir -p "$DEST_DIR$SYSCONF_DIR/fw-fanctrl"
build
if [ "$NO_PIP_INSTALL" = false ]; then
echo "installing python package"
if [ "$PIPX" = false ]; then
python3 -m pip install --prefix="$PYTHON_PREFIX_DIRECTORY" dist/*.tar.gz
which python3
else
pipx install --global --force dist/*.tar.gz
fi
which 'fw-fanctrl' 2> "/dev/null" || true
remove_target "dist/"
fi
cp -pn "./src/fw_fanctrl/_resources/config.json" "$DEST_DIR$SYSCONF_DIR/fw-fanctrl" 2> "/dev/null" || true
cp -f "./src/fw_fanctrl/_resources/config.schema.json" "$DEST_DIR$SYSCONF_DIR/fw-fanctrl" 2> "/dev/null" || true
# add --no-battery-sensors flag to the fanctrl service if specified
if [ "$NO_BATTERY_SENSOR" = true ]; then
NO_BATTERY_SENSOR_OPTION="--no-battery-sensors"
fi
# create program services based on the services present in the './services' folder
echo "creating '$DEST_DIR$PREFIX_DIR/lib/systemd/system'"
mkdir -p "$DEST_DIR$PREFIX_DIR/lib/systemd/system"
echo "creating services"
for SERVICE in $SERVICES ; do
SERVICE=$(sanitizePath "$SERVICE")
if [ "$SHOULD_PRE_UNINSTALL" = true ] && [ "$(systemctl is-active "$SERVICE")" == "active" ]; then
echo "stopping [$SERVICE]"
systemctl stop "$SERVICE"
fi
echo "creating '$DEST_DIR$PREFIX_DIR/lib/systemd/system/$SERVICE$SERVICE_EXTENSION'"
cat "$SERVICES_DIR/$SERVICE$SERVICE_EXTENSION" | sed -e "s/%PYTHON_SCRIPT_INSTALLATION_PATH%/${PYTHON_SCRIPT_INSTALLATION_PATH//\//\\/}/" | sed -e "s/%SYSCONF_DIRECTORY%/${SYSCONF_DIR//\//\\/}/" | sed -e "s/%NO_BATTERY_SENSOR_OPTION%/${NO_BATTERY_SENSOR_OPTION}/" | tee "$DEST_DIR$PREFIX_DIR/lib/systemd/system/$SERVICE$SERVICE_EXTENSION" > "/dev/null"
done
# add program services sub-configurations based on the sub-configurations present in the './services' folder
echo "adding services sub-configurations"
for SERVICE in $SERVICES_SUBCONFIGS ; do
SERVICE=$(sanitizePath "$SERVICE")
echo "adding sub-configurations for [$SERVICE]"
SUBCONFIG_FOLDERS="$(cd "$SERVICES_DIR/$SERVICE" && find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)"
# ensure folders exists
mkdir -p "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE"
for SUBCONFIG_FOLDER in $SUBCONFIG_FOLDERS ; do
SUBCONFIG_FOLDER=$(sanitizePath "$SUBCONFIG_FOLDER")
echo "creating '$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG_FOLDER'"
mkdir -p "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG_FOLDER"
done
SUBCONFIGS="$(cd "$SERVICES_DIR/$SERVICE" && find . -mindepth 1 -type f)"
# add sub-configurations
for SUBCONFIG in $SUBCONFIGS ; do
SUBCONFIG=$(sanitizePath "$SUBCONFIG")
echo "adding '$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG'"
cat "$SERVICES_DIR/$SERVICE/$SUBCONFIG" | sed -e "s/%PYTHON_SCRIPT_INSTALLATION_PATH%/${PYTHON_SCRIPT_INSTALLATION_PATH//\//\\/}/" | tee "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG" > "/dev/null"
chmod +x "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG"
done
done
if [ "$SHOULD_POST_INSTALL" = true ]; then
if ! ./post-install.sh --dest-dir "$DEST_DIR" --sysconf-dir "$SYSCONF_DIR" "$([ "$NO_SUDO" = true ] && echo "--no-sudo")"; then
echo "Failed to run ./post-install.sh. Run the script with root permissions,"
echo "or skip this step by using the --no-post-install option."
exit 1
fi
fi
}
function installEctool() {
workingDirectory=$1
echo "installing ectool"
ectoolDestPath="$DEST_DIR$PREFIX_DIR/bin/ectool"
ectoolJobId="$(cat './fetch/ectool/linux/gitlab_job_id')"
ectoolSha256Hash="$(cat './fetch/ectool/linux/hash.sha256')"
artifactsZipFile="$workingDirectory/artifact.zip"
echo "downloading artifact from gitlab"
curl -s -S -o "$artifactsZipFile" -L "https://gitlab.howett.net/DHowett/ectool/-/jobs/${ectoolJobId}/artifacts/download?file_type=archive" || (echo "failed to download the artifact." && return 1)
if [[ $? -ne 0 ]]; then return 1; fi
echo "checking artifact sha256 sum"
actualEctoolSha256Hash=$(sha256sum "$artifactsZipFile" | cut -d ' ' -f 1)
if [[ "$actualEctoolSha256Hash" != "$ectoolSha256Hash" ]]; then
echo "Incorrect sha256 sum for ectool gitlab artifact '$ectoolJobId' : '$ectoolSha256Hash' != '$actualEctoolSha256Hash'"
return 1
fi
echo "extracting artifact"
{
unzip -q -j "$artifactsZipFile" '_build/src/ectool' -d "$workingDirectory" &&
cp "$workingDirectory/ectool" "$ectoolDestPath" &&
chmod +x "$ectoolDestPath"
} || (echo "failed to extract the artifact to its designated location." && return 1)
if [[ $? -ne 0 ]]; then return 1; fi
echo "ectool installed"
}
if [ "$SHOULD_REMOVE" = true ]; then
uninstall
else
install
fi
exit 0
================================================
FILE: post-install.sh
================================================
#!/bin/bash
set -e
HOME_DIR="$(eval echo "~$(logname)")"
# Argument parsing
NO_SUDO=false
SHORT=d:,s:,h
LONG=dest-dir:,sysconf-dir:,no-sudo,help
VALID_ARGS=$(getopt -a --options $SHORT --longoptions $LONG -- "$@")
if [[ $? -ne 0 ]]; then
exit 1;
fi
DEST_DIR="/usr"
SYSCONF_DIR="/etc"
eval set -- "$VALID_ARGS"
while true; do
case "$1" in
'--dest-dir' | '-d')
DEST_DIR=$2
shift
;;
'--sysconf-dir' | '-s')
SYSCONF_DIR=$2
shift
;;
'--no-sudo')
NO_SUDO=true
;;
'--help' | '-h')
echo "Usage: $0 [--dest-dir,-d <installation destination directory (defaults to $DEST_DIR)>] [--sysconf-dir,-s system configuration destination directory (defaults to $SYSCONF_DIR)] [--no-sudo]" 1>&2
exit 0
;;
--)
break
;;
esac
shift
done
# Root check
if [ "$EUID" -ne 0 ] && [ "$NO_SUDO" = false ]
then echo "This program requires root permissions or use the '--no-sudo' option"
exit 1
fi
SERVICES_DIR="./services"
SERVICE_EXTENSION=".service"
SERVICES="$(cd "$SERVICES_DIR" && find . -maxdepth 1 -maxdepth 1 -type f -name "*$SERVICE_EXTENSION" -exec basename {} "$SERVICE_EXTENSION" \;)"
function sanitizePath() {
local SANITIZED_PATH="$1"
local SANITIZED_PATH=${SANITIZED_PATH//..\//}
local SANITIZED_PATH=${SANITIZED_PATH#./}
local SANITIZED_PATH=${SANITIZED_PATH#/}
echo "$SANITIZED_PATH"
}
# move remaining legacy files
function move_legacy() {
echo "moving legacy files to their new destination"
(cp "$HOME_DIR/.config/fw-fanctrl"/* "$DEST_DIR$SYSCONF_DIR/fw-fanctrl/" && rm -rf "$HOME_DIR/.config/fw-fanctrl") 2> "/dev/null" || true
}
move_legacy
echo "enabling services"
systemctl daemon-reload
for SERVICE in $SERVICES ; do
SERVICE=$(sanitizePath "$SERVICE")
echo "enabling [$SERVICE]"
systemctl enable "$SERVICE"
echo "starting [$SERVICE]"
systemctl start "$SERVICE"
done
================================================
FILE: pre-uninstall.sh
================================================
#!/bin/bash
set -e
# Argument parsing
NO_SUDO=false
SHORT=h
LONG=no-sudo,help
VALID_ARGS=$(getopt -a --options $SHORT --longoptions $LONG -- "$@")
if [[ $? -ne 0 ]]; then
exit 1;
fi
eval set -- "$VALID_ARGS"
while true; do
case "$1" in
'--no-sudo')
NO_SUDO=true
;;
'--help' | '-h')
echo "Usage: $0 [--no-sudo]" 1>&2
exit 0
;;
--)
break
;;
esac
shift
done
if [ "$EUID" -ne 0 ] && [ "$NO_SUDO" = false ]
then echo "This program requires root permissions or use the '--no-sudo' option"
exit 1
fi
SERVICES_DIR="./services"
SERVICE_EXTENSION=".service"
SERVICES="$(cd "$SERVICES_DIR" && find . -maxdepth 1 -maxdepth 1 -type f -name "*$SERVICE_EXTENSION" -exec basename {} "$SERVICE_EXTENSION" \;)"
function sanitizePath() {
local SANITIZED_PATH="$1"
local SANITIZED_PATH=${SANITIZED_PATH//..\//}
local SANITIZED_PATH=${SANITIZED_PATH#./}
local SANITIZED_PATH=${SANITIZED_PATH#/}
echo "$SANITIZED_PATH"
}
echo "disabling services"
systemctl daemon-reload
for SERVICE in $SERVICES ; do
SERVICE=$(sanitizePath "$SERVICE")
echo "stopping [$SERVICE]"
systemctl stop "$SERVICE" 2> "/dev/null" || true
echo "disabling [$SERVICE]"
systemctl disable "$SERVICE" 2> "/dev/null" || true
done
================================================
FILE: pyproject.toml
================================================
[tool.black]
line-length = 120
exclude = '''
/(
\.git
| \.github
| \.idea
| \.vscode
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| services
| fetch
| \.temp
)/
'''
[build-system]
requires = ["setuptools>=75.2.0"]
build-backend = "setuptools.build_meta"
[project]
name = "fw-fanctrl"
version = "1.0.4"
description = "A simple systemd service to better control Framework Laptop's fan(s)."
keywords = ["framework", "laptop", "fan", "control", "cli", "service"]
readme = "README.md"
authors = [
{ name = "TamtamHero" },
]
maintainers = [
{ name = "TamtamHero" },
{ name = "leopoldhub" },
]
license = { file = "LICENSE" }
classifiers = [
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Operating System :: POSIX :: Linux",
"Topic :: System :: Hardware",
]
requires-python = ">=3.12"
dependencies = [
"jsonschema==4.*"
]
optional-dependencies = { dev = [
"black==24.8.0",
"build>=1.2.2.post1",
"setuptools>=75.2.0",
] }
[project.urls]
Homepage = "https://github.com/TamtamHero/fw-fanctrl"
Documentation = "https://github.com/TamtamHero/fw-fanctrl"
Repository = "https://github.com/TamtamHero/fw-fanctrl.git"
Issues = "https://github.com/TamtamHero/fw-fanctrl/issues"
[tool.setuptools]
package-dir = { "" = "src" }
[tool.setuptools.package-data]
"fw_fanctrl" = ["_resources/**/*"]
[project.scripts]
fw-fanctrl = "fw_fanctrl.__main__:main"
================================================
FILE: services/fw-fanctrl.service
================================================
[Unit]
Description=Framework Fan Controller
After=multi-user.target
[Service]
Type=simple
Restart=always
ExecStart="%PYTHON_SCRIPT_INSTALLATION_PATH%" --output-format "JSON" run --config "%SYSCONF_DIRECTORY%/fw-fanctrl/config.json" --silent %NO_BATTERY_SENSOR_OPTION%
ExecStopPost=/bin/sh -c "ectool autofanctrl"
[Install]
WantedBy=multi-user.target
================================================
FILE: services/system-sleep/fw-fanctrl-suspend
================================================
#!/bin/sh
case $1 in
pre) "%PYTHON_SCRIPT_INSTALLATION_PATH%" pause ;;
post) "%PYTHON_SCRIPT_INSTALLATION_PATH%" resume ;;
esac
================================================
FILE: src/fw_fanctrl/CommandParser.py
================================================
import argparse
import os
import sys
import textwrap
from fw_fanctrl import DEFAULT_CONFIGURATION_FILE_PATH
from fw_fanctrl.enum.OutputFormat import OutputFormat
from fw_fanctrl.exception.UnknownCommandException import UnknownCommandException
class CommandParser:
is_remote = True
legacy_parser = None
parser = None
def __init__(self, is_remote=False):
self.is_remote = is_remote
self.init_parser()
self.init_legacy_parser()
def init_parser(self):
self.parser = argparse.ArgumentParser(
prog="fw-fanctrl",
description="control Framework's laptop fan(s) with a speed curve",
epilog=textwrap.dedent(
"obtain more help about a command or subcommand using `fw-fanctrl <command> [subcommand...] -h/--help`"
),
formatter_class=argparse.RawTextHelpFormatter,
)
self.parser.add_argument(
"--socket-controller",
"--sc",
help="the socket controller to use for communication between the cli and the service",
type=str,
choices=["unix"],
default="unix",
)
self.parser.add_argument(
"--output-format",
help="the output format to use for the command result",
type=lambda s: (lambda: OutputFormat[s])() if hasattr(OutputFormat, s) else s,
choices=list(OutputFormat._member_names_),
default=OutputFormat.NATURAL,
)
commands_sub_parser = self.parser.add_subparsers(dest="command")
commands_sub_parser.required = True
if not self.is_remote:
run_command = commands_sub_parser.add_parser(
"run",
description="run the service",
formatter_class=argparse.RawTextHelpFormatter,
)
run_command.add_argument(
"strategy",
help='name of the strategy to use e.g: "lazy" (use `print strategies` to list available strategies)',
nargs=argparse.OPTIONAL,
)
run_command.add_argument(
"--config",
"-c",
help=f"the configuration file path (default: {DEFAULT_CONFIGURATION_FILE_PATH})",
type=str,
default=DEFAULT_CONFIGURATION_FILE_PATH,
)
run_command.add_argument(
"--silent",
"-s",
help="disable printing speed/temp status to stdout",
action="store_true",
)
run_command.add_argument(
"--hardware-controller",
"--hc",
help="the hardware controller to use for fetching and setting the temp and fan(s) speed",
type=str,
choices=["ectool"],
default="ectool",
)
run_command.add_argument(
"--no-battery-sensors",
help="disable checking battery temperature sensors",
action="store_true",
)
use_command = commands_sub_parser.add_parser("use", description="change the current strategy")
use_command.add_argument(
"strategy",
help='name of the strategy to use e.g: "lazy". (use `print list` to list available strategies)',
)
commands_sub_parser.add_parser("reset", description="reset to the default strategy")
commands_sub_parser.add_parser("reload", description="reload the configuration file")
commands_sub_parser.add_parser("pause", description="pause the service")
commands_sub_parser.add_parser("resume", description="resume the service")
print_command = commands_sub_parser.add_parser(
"print",
description="print the selected information",
formatter_class=argparse.RawTextHelpFormatter,
)
print_command.add_argument(
"print_selection",
help=f"all - All details{os.linesep}current - The current strategy{os.linesep}list - List available strategies{os.linesep}speed - The current fan speed percentage{os.linesep}active - The service activity status",
nargs="?",
type=str,
choices=["all", "active", "current", "list", "speed"],
default="all",
)
set_config_command = commands_sub_parser.add_parser(
"set_config", description="replace the service configuration with the provided one"
)
set_config_command.add_argument(
"provided_config",
help="must be a valid JSON configuration",
type=str,
)
def init_legacy_parser(self):
self.legacy_parser = argparse.ArgumentParser(add_help=False)
# avoid collision with the new parser commands
def excluded_positional_arguments(value):
if value in [
"run",
"use",
"reload",
"reset",
"pause",
"resume",
"print",
"set_config",
]:
raise argparse.ArgumentTypeError("%s is an excluded value" % value)
return value
both_group = self.legacy_parser.add_argument_group("both")
both_group.add_argument("_strategy", nargs="?", type=excluded_positional_arguments)
both_group.add_argument("--strategy", nargs="?")
run_group = self.legacy_parser.add_argument_group("run")
run_group.add_argument("--run", action="store_true")
run_group.add_argument("--config", type=str, default=DEFAULT_CONFIGURATION_FILE_PATH)
run_group.add_argument("--no-log", action="store_true")
command_group = self.legacy_parser.add_argument_group("configure")
command_group.add_argument("--query", "-q", action="store_true")
command_group.add_argument("--list-strategies", action="store_true")
command_group.add_argument("--reload", "-r", action="store_true")
command_group.add_argument("--pause", action="store_true")
command_group.add_argument("--resume", action="store_true")
command_group.add_argument(
"--hardware-controller",
"--hc",
type=str,
choices=["ectool"],
default="ectool",
)
command_group.add_argument(
"--socket-controller",
"--sc",
type=str,
choices=["unix"],
default="unix",
)
def parse_args(self, args=None):
original_stderr = sys.stderr
# silencing legacy parser output
sys.stderr = open(os.devnull, "w")
try:
legacy_values = self.legacy_parser.parse_args(args)
if legacy_values.strategy is None:
legacy_values.strategy = legacy_values._strategy
# converting legacy values into new ones
values = argparse.Namespace()
values.socket_controller = legacy_values.socket_controller
values.output_format = OutputFormat.NATURAL
if legacy_values.query:
values.command = "print"
values.print_selection = "current"
if legacy_values.list_strategies:
values.command = "print"
values.print_selection = "list"
if legacy_values.resume:
values.command = "resume"
if legacy_values.pause:
values.command = "pause"
if legacy_values.reload:
values.command = "reload"
if legacy_values.run:
values.command = "run"
values.silent = legacy_values.no_log
values.hardware_controller = legacy_values.hardware_controller
values.config = legacy_values.config
values.strategy = legacy_values.strategy
if not hasattr(values, "command") and legacy_values.strategy is not None:
values.command = "use"
values.strategy = legacy_values.strategy
if not hasattr(values, "command"):
raise UnknownCommandException("not a valid legacy command")
if self.is_remote or values.command == "run":
# Legacy commands do not support other formats than NATURAL, so there is no need to use a CommandResult.
print(
"[Warning] > this command is deprecated and will be removed soon, please use the new command format instead ('fw-fanctrl -h' for more details)."
)
except (SystemExit, Exception):
sys.stderr = original_stderr
values = self.parser.parse_args(args)
finally:
sys.stderr = original_stderr
return values
================================================
FILE: src/fw_fanctrl/Configuration.py
================================================
import json
from json import JSONDecodeError
from os.path import isfile
from shutil import copyfile
import jsonschema
from fw_fanctrl import INTERNAL_RESOURCES_PATH
from fw_fanctrl.Strategy import Strategy
from fw_fanctrl.exception.ConfigurationParsingException import ConfigurationParsingException
from fw_fanctrl.exception.InvalidStrategyException import InvalidStrategyException
VALIDATION_SCHEMA_PATH = INTERNAL_RESOURCES_PATH.joinpath("config.schema.json")
ORIGINAL_CONFIG_PATH = INTERNAL_RESOURCES_PATH.joinpath("config.json")
class Configuration:
path = None
data = None
def __init__(self, path):
self.path = path
self.reload()
def parse(self, raw_config):
try:
config = json.loads(raw_config)
if "$schema" not in config:
original_config = json.load(ORIGINAL_CONFIG_PATH.open("r"))
config["$schema"] = original_config["$schema"]
jsonschema.Draft202012Validator(json.load(VALIDATION_SCHEMA_PATH.open("r"))).validate(config)
if config["defaultStrategy"] not in config["strategies"]:
raise ConfigurationParsingException(
f"Default strategy '{config["defaultStrategy"]}' is not a valid strategy."
)
if config["strategyOnDischarging"] != "" and config["strategyOnDischarging"] not in config["strategies"]:
raise ConfigurationParsingException(
f"Discharging strategy '{config['strategyOnDischarging']}' is not a valid strategy."
)
return config
except JSONDecodeError as e:
raise ConfigurationParsingException(f"Error parsing configuration file: {e}")
def reload(self):
if not isfile(self.path):
copyfile(ORIGINAL_CONFIG_PATH, self.path)
with open(self.path, "r") as fp:
raw_config = fp.read()
self.data = self.parse(raw_config)
def save(self):
string_config = json.dumps(self.data, indent=4)
with open(self.path, "w") as fp:
fp.write(string_config)
def get_strategies(self):
return self.data["strategies"].keys()
def get_strategy(self, strategy_name):
if strategy_name == "strategyOnDischarging":
strategy_name = self.data[strategy_name]
if strategy_name == "":
strategy_name = "defaultStrategy"
if strategy_name == "defaultStrategy":
strategy_name = self.data[strategy_name]
if strategy_name is None or strategy_name not in self.data["strategies"]:
raise InvalidStrategyException(strategy_name)
return Strategy(strategy_name, self.data["strategies"][strategy_name])
def get_default_strategy(self):
return self.get_strategy("defaultStrategy")
def get_discharging_strategy(self):
return self.get_strategy("strategyOnDischarging")
================================================
FILE: src/fw_fanctrl/FanController.py
================================================
import collections
import sys
import threading
from time import sleep
from fw_fanctrl.Configuration import Configuration
from fw_fanctrl.dto.command_result.ConfigurationReloadCommandResult import ConfigurationReloadCommandResult
from fw_fanctrl.dto.command_result.PrintActiveCommandResult import PrintActiveCommandResult
from fw_fanctrl.dto.command_result.PrintCurrentStrategyCommandResult import PrintCurrentStrategyCommandResult
from fw_fanctrl.dto.command_result.PrintFanSpeedCommandResult import PrintFanSpeedCommandResult
from fw_fanctrl.dto.command_result.PrintStrategyListCommandResult import PrintStrategyListCommandResult
from fw_fanctrl.dto.command_result.ServicePauseCommandResult import ServicePauseCommandResult
from fw_fanctrl.dto.command_result.ServiceResumeCommandResult import ServiceResumeCommandResult
from fw_fanctrl.dto.command_result.SetConfigurationCommandResult import SetConfigurationCommandResult
from fw_fanctrl.dto.command_result.StrategyChangeCommandResult import StrategyChangeCommandResult
from fw_fanctrl.dto.command_result.StrategyResetCommandResult import StrategyResetCommandResult
from fw_fanctrl.dto.runtime_result.RuntimeResult import RuntimeResult
from fw_fanctrl.dto.runtime_result.StatusRuntimeResult import StatusRuntimeResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
from fw_fanctrl.exception.InvalidStrategyException import InvalidStrategyException
from fw_fanctrl.exception.UnknownCommandException import UnknownCommandException
class FanController:
hardware_controller = None
socket_controller = None
configuration = None
overwritten_strategy = None
output_format = None
speed = 0
temp_history = collections.deque([0] * 100, maxlen=100)
active = True
timecount = 0
def __init__(self, hardware_controller, socket_controller, config_path, strategy_name, output_format):
self.hardware_controller = hardware_controller
self.socket_controller = socket_controller
self.configuration = Configuration(config_path)
if strategy_name is not None and strategy_name != "":
self.overwrite_strategy(strategy_name)
self.output_format = output_format
t = threading.Thread(
target=self.socket_controller.start_server_socket,
args=[self.command_manager],
)
t.daemon = True
t.start()
def get_actual_temperature(self):
return self.hardware_controller.get_temperature()
def set_speed(self, speed):
self.speed = speed
self.hardware_controller.set_speed(speed)
def is_on_ac(self):
return self.hardware_controller.is_on_ac()
def pause(self):
self.active = False
self.hardware_controller.pause()
def resume(self):
self.active = True
self.hardware_controller.resume()
def overwrite_strategy(self, strategy_name):
if strategy_name not in self.configuration.get_strategies():
self.clear_overwritten_strategy()
return
self.overwritten_strategy = self.configuration.get_strategy(strategy_name)
self.timecount = 0
def clear_overwritten_strategy(self):
self.overwritten_strategy = None
self.timecount = 0
def get_current_strategy(self):
if self.overwritten_strategy is not None:
return self.overwritten_strategy
if self.is_on_ac():
return self.configuration.get_default_strategy()
return self.configuration.get_discharging_strategy()
def command_manager(self, args):
if args.command == "reset" or (args.command == "use" and args.strategy == "defaultStrategy"):
self.clear_overwritten_strategy()
return StrategyResetCommandResult(self.get_current_strategy().name, self.overwritten_strategy is None)
elif args.command == "use":
if args.strategy not in self.configuration.get_strategies():
raise InvalidStrategyException(f"The specified strategy is invalid: {args.strategy}")
self.overwrite_strategy(args.strategy)
return StrategyChangeCommandResult(self.get_current_strategy().name, self.overwritten_strategy is None)
elif args.command == "reload":
self.configuration.reload()
if self.overwritten_strategy is not None:
self.overwrite_strategy(self.overwritten_strategy.name)
return ConfigurationReloadCommandResult(self.get_current_strategy().name, self.overwritten_strategy is None)
elif args.command == "pause":
self.pause()
return ServicePauseCommandResult()
elif args.command == "resume":
self.resume()
return ServiceResumeCommandResult(self.get_current_strategy().name, self.overwritten_strategy is None)
elif args.command == "print":
if args.print_selection == "all":
return self.dump_details()
elif args.print_selection == "active":
return PrintActiveCommandResult(self.active)
elif args.print_selection == "current":
return PrintCurrentStrategyCommandResult(
self.get_current_strategy().name, self.overwritten_strategy is None
)
elif args.print_selection == "list":
return PrintStrategyListCommandResult(list(self.configuration.get_strategies()))
elif args.print_selection == "speed":
return PrintFanSpeedCommandResult(str(self.speed))
elif args.command == "set_config":
self.configuration.data = self.configuration.parse(args.provided_config)
if self.overwritten_strategy is not None:
self.overwrite_strategy(self.overwritten_strategy.name)
self.configuration.save()
return SetConfigurationCommandResult(
self.get_current_strategy().name, vars(self.configuration), self.overwritten_strategy is None
)
raise UnknownCommandException(f"Unknown command: '{args.command}', unexpected.")
# return mean temperature over a given time interval (in seconds)
def get_moving_average_temperature(self, time_interval):
sliced_temp_history = [x for x in self.temp_history if x > 0][-time_interval:]
if len(sliced_temp_history) == 0:
return self.get_actual_temperature()
return float(round(sum(sliced_temp_history) / len(sliced_temp_history), 2))
def get_effective_temperature(self, current_temp, time_interval):
# the moving average temperature count for 2/3 of the effective temperature
return float(round(min(self.get_moving_average_temperature(time_interval), current_temp), 2))
def adapt_speed(self, current_temp):
current_strategy = self.get_current_strategy()
current_temp = self.get_effective_temperature(current_temp, current_strategy.moving_average_interval)
min_point = current_strategy.speed_curve[0]
max_point = current_strategy.speed_curve[-1]
for e in current_strategy.speed_curve:
if current_temp > e["temp"]:
min_point = e
else:
max_point = e
break
if min_point == max_point:
new_speed = min_point["speed"]
else:
slope = (max_point["speed"] - min_point["speed"]) / (max_point["temp"] - min_point["temp"])
new_speed = int(min_point["speed"] + (current_temp - min_point["temp"]) * slope)
if self.active:
self.set_speed(new_speed)
def dump_details(self):
current_strategy = self.get_current_strategy()
current_temperature = self.get_actual_temperature()
moving_average_temp = self.get_moving_average_temperature(current_strategy.moving_average_interval)
effective_temp = self.get_effective_temperature(current_temperature, current_strategy.moving_average_interval)
return StatusRuntimeResult(
current_strategy.name,
self.overwritten_strategy is None,
self.speed,
current_temperature,
moving_average_temp,
effective_temp,
self.active,
vars(self.configuration),
)
def print_state(self):
print(self.dump_details().to_output_format(self.output_format))
def run(self, debug=True):
try:
while True:
if self.active:
temp = self.get_actual_temperature()
# update fan speed every "fanSpeedUpdateFrequency" seconds
if self.timecount % self.get_current_strategy().fan_speed_update_frequency == 0:
self.adapt_speed(temp)
self.timecount = 0
self.temp_history.append(temp)
if debug:
self.print_state()
self.timecount += 1
sleep(1)
else:
sleep(5)
except InvalidStrategyException as e:
_rte = RuntimeResult(CommandStatus.ERROR, f"Missing strategy, exiting for safety reasons: {e.args[0]}")
print(_rte.to_output_format(self.output_format), file=sys.stderr)
except Exception as e:
_rte = RuntimeResult(CommandStatus.ERROR, f"Critical error, exiting for safety reasons: {e}")
print(_rte.to_output_format(self.output_format), file=sys.stderr)
exit(1)
================================================
FILE: src/fw_fanctrl/Strategy.py
================================================
class Strategy:
name = None
fan_speed_update_frequency = None
moving_average_interval = None
speed_curve = None
def __init__(self, name, parameters):
self.name = name
self.fan_speed_update_frequency = parameters["fanSpeedUpdateFrequency"]
if self.fan_speed_update_frequency is None or self.fan_speed_update_frequency == "":
self.fan_speed_update_frequency = 5
self.moving_average_interval = parameters["movingAverageInterval"]
if self.moving_average_interval is None or self.moving_average_interval == "":
self.moving_average_interval = 20
self.speed_curve = parameters["speedCurve"]
================================================
FILE: src/fw_fanctrl/__init__.py
================================================
import importlib.resources
import os
INTERNAL_RESOURCES_PATH = importlib.resources.files("fw_fanctrl").joinpath("_resources")
DEFAULT_CONFIGURATION_FILE_PATH = "/etc/fw-fanctrl/config.json"
SOCKETS_FOLDER_PATH = "/run/fw-fanctrl"
COMMANDS_SOCKET_FILE_PATH = os.path.join(SOCKETS_FOLDER_PATH, ".fw-fanctrl.commands.sock")
================================================
FILE: src/fw_fanctrl/__main__.py
================================================
import shlex
import sys
from fw_fanctrl.CommandParser import CommandParser
from fw_fanctrl.FanController import FanController
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
from fw_fanctrl.enum.OutputFormat import OutputFormat
from fw_fanctrl.hardwareController.EctoolHardwareController import EctoolHardwareController
from fw_fanctrl.socketController.UnixSocketController import UnixSocketController
def main():
try:
args = CommandParser().parse_args(shlex.split(shlex.join(sys.argv[1:])))
except Exception as e:
_cre = CommandResult(CommandStatus.ERROR, str(e))
print(_cre.to_output_format(OutputFormat.NATURAL), file=sys.stderr)
exit(1)
socket_controller = UnixSocketController()
if args.socket_controller == "unix":
socket_controller = UnixSocketController()
if args.command == "run":
hardware_controller = EctoolHardwareController(no_battery_sensor_mode=args.no_battery_sensors)
if args.hardware_controller == "ectool":
hardware_controller = EctoolHardwareController(no_battery_sensor_mode=args.no_battery_sensors)
fan = FanController(
hardware_controller=hardware_controller,
socket_controller=socket_controller,
config_path=args.config,
strategy_name=args.strategy,
output_format=getattr(args, "output_format", None),
)
fan.run(debug=not args.silent)
else:
try:
command_result = socket_controller.send_via_client_socket(shlex.join(sys.argv[1:]))
if command_result:
print(command_result)
except Exception as e:
if str(e).startswith("[Error] >"):
print(str(e), file=sys.stderr)
else:
_cre = CommandResult(CommandStatus.ERROR, str(e))
print(_cre.to_output_format(getattr(args, "output_format", None)), file=sys.stderr)
exit(1)
if __name__ == "__main__":
main()
================================================
FILE: src/fw_fanctrl/_resources/config.json
================================================
{
"$schema": "./config.schema.json",
"defaultStrategy": "lazy",
"strategyOnDischarging" : "",
"strategies": {
"laziest": {
"fanSpeedUpdateFrequency": 5,
"movingAverageInterval": 40,
"speedCurve": [
{ "temp": 0, "speed": 0 },
{ "temp": 45, "speed": 0 },
{ "temp": 65, "speed": 25 },
{ "temp": 70, "speed": 35 },
{ "temp": 75, "speed": 50 },
{ "temp": 85, "speed": 100 }
]
},
"lazy": {
"fanSpeedUpdateFrequency": 5,
"movingAverageInterval": 30,
"speedCurve": [
{ "temp": 0, "speed": 15 },
{ "temp": 50, "speed": 15 },
{ "temp": 65, "speed": 25 },
{ "temp": 70, "speed": 35 },
{ "temp": 75, "speed": 50 },
{ "temp": 85, "speed": 100 }
]
},
"medium": {
"fanSpeedUpdateFrequency": 5,
"movingAverageInterval": 30,
"speedCurve": [
{ "temp": 0, "speed": 15 },
{ "temp": 40, "speed": 15 },
{ "temp": 60, "speed": 30 },
{ "temp": 70, "speed": 40 },
{ "temp": 75, "speed": 80 },
{ "temp": 85, "speed": 100 }
]
},
"agile": {
"fanSpeedUpdateFrequency": 3,
"movingAverageInterval": 15,
"speedCurve": [
{ "temp": 0, "speed": 15 },
{ "temp": 40, "speed": 15 },
{ "temp": 60, "speed": 30 },
{ "temp": 70, "speed": 40 },
{ "temp": 75, "speed": 80 },
{ "temp": 85, "speed": 100 }
]
},
"very-agile": {
"fanSpeedUpdateFrequency": 2,
"movingAverageInterval": 5,
"speedCurve": [
{ "temp": 0, "speed": 15 },
{ "temp": 40, "speed": 15 },
{ "temp": 60, "speed": 30 },
{ "temp": 70, "speed": 40 },
{ "temp": 75, "speed": 80 },
{ "temp": 85, "speed": 100 }
]
},
"deaf": {
"fanSpeedUpdateFrequency": 2,
"movingAverageInterval": 5,
"speedCurve": [
{ "temp": 0, "speed": 20 },
{ "temp": 40, "speed": 30 },
{ "temp": 50, "speed": 50 },
{ "temp": 60, "speed": 100 }
]
},
"aeolus": {
"fanSpeedUpdateFrequency": 2,
"movingAverageInterval": 5,
"speedCurve": [
{ "temp": 0, "speed": 20 },
{ "temp": 40, "speed": 50 },
{ "temp": 65, "speed": 100 }
]
}
}
}
================================================
FILE: src/fw_fanctrl/_resources/config.schema.json
================================================
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "file://config.schema.json",
"title": "fw-fanctrl configuration",
"description": "Configuration for the fw-fanctrl service, defining fan control strategies based on temperature thresholds.",
"type": "object",
"patternProperties": {
"^\\$schema$": {
"type": "string",
"format": "uri",
"description": "Points to the JSON Schema to use for validation.",
"pattern": "^\\./config\\.schema\\.json$"
}
},
"properties": {
"defaultStrategy": {
"description": "The default strategy to use.",
"$comment": "Must match a key in the `strategies` object.",
"$ref": "#/$defs/strategyKey"
},
"strategyOnDischarging": {
"description": "The strategy to use when the system is on battery power. Use an empty string to disable special behavior.",
"oneOf": [
{
"type": "string",
"const": "",
"description": "Disables battery-specific behavior (use default strategy)."
},
{
"$comment": "Must match a key in the `strategies` object.",
"$ref": "#/$defs/strategyKey"
}
]
},
"strategies": {
"description": "A collection of named fan control strategies.",
"type": "object",
"minProperties": 1,
"additionalProperties": {
"$ref": "#/$defs/strategy"
},
"propertyNames": {
"$ref": "#/$defs/strategyKey"
}
}
},
"required": [
"defaultStrategy",
"strategyOnDischarging",
"strategies",
"$schema"
],
"additionalProperties": false,
"$defs": {
"strategyKey": {
"type": "string",
"pattern": "^[a-zA-Z0-9_-]+$",
"description": "A unique identifier for a strategy (alphanumeric, underscores, and hyphens allowed).",
"examples": [
"lazy",
"agile",
"deaf"
]
},
"strategy": {
"type": "object",
"description": "A strategy defines how fan speed is adjusted based on temperature readings.",
"properties": {
"fanSpeedUpdateFrequency": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"description": "How frequently (in seconds) to update the fan speed."
},
"movingAverageInterval": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "The time window (in seconds) over which temperature readings are averaged."
},
"speedCurve": {
"type": "array",
"minItems": 1,
"description": "A list of temperature-speed pairs defining the fan response curve. Should be sorted by ascending `temp`.",
"items": {
"type": "object",
"properties": {
"temp": {
"type": "number",
"multipleOf" : 0.01,
"minimum": 0,
"maximum": 100,
"description": "Temperature threshold (in degrees Celsius) up to two digit precision (e.g. 15.23)."
},
"speed": {
"type": "integer",
"minimum": 0,
"maximum": 100,
"description": "Fan speed (in percent) to set when the temperature reaches this threshold."
}
},
"required": [
"temp",
"speed"
],
"additionalProperties": false
},
"uniqueItems": true,
"minProperties": 1,
"examples": [
{
"temp": 0,
"speed": 0
},
{
"temp": 65,
"speed": 25
},
{
"temp": 85,
"speed": 100
}
]
}
},
"required": [
"speedCurve"
],
"additionalProperties": false
}
}
}
================================================
FILE: src/fw_fanctrl/dto/Printable.py
================================================
import json
from fw_fanctrl.enum.OutputFormat import OutputFormat
class Printable:
def __init__(self):
super().__init__()
def to_output_format(self, output_format):
if output_format == OutputFormat.JSON:
return json.dumps(self.__dict__)
else:
return str(self)
================================================
FILE: src/fw_fanctrl/dto/__init__.py
================================================
================================================
FILE: src/fw_fanctrl/dto/command_result/CommandResult.py
================================================
from fw_fanctrl.dto.Printable import Printable
from fw_fanctrl.enum.CommandStatus import CommandStatus
class CommandResult(Printable):
def __init__(self, status, reason="Unexpected"):
super().__init__()
self.status = status
if status == CommandStatus.ERROR:
self.reason = reason
def __str__(self):
return "Success!" if self.status == CommandStatus.SUCCESS else f"[Error] > An error occurred: {self.reason}"
================================================
FILE: src/fw_fanctrl/dto/command_result/ConfigurationReloadCommandResult.py
================================================
import os
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class ConfigurationReloadCommandResult(CommandResult):
def __init__(self, strategy, default):
super().__init__(CommandStatus.SUCCESS)
self.strategy = strategy
self.default = default
def __str__(self):
return f"Reloaded with success! Strategy in use: '{self.strategy}'{os.linesep}" f"Default: {self.default}"
================================================
FILE: src/fw_fanctrl/dto/command_result/PrintActiveCommandResult.py
================================================
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class PrintActiveCommandResult(CommandResult):
def __init__(self, active):
super().__init__(CommandStatus.SUCCESS)
self.active = active
def __str__(self):
return f"Active: {self.active}"
================================================
FILE: src/fw_fanctrl/dto/command_result/PrintCurrentStrategyCommandResult.py
================================================
import os
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class PrintCurrentStrategyCommandResult(CommandResult):
def __init__(self, strategy, default):
super().__init__(CommandStatus.SUCCESS)
self.strategy = strategy
self.default = default
def __str__(self):
return f"Strategy in use: '{self.strategy}'{os.linesep}" f"Default: {self.default}"
================================================
FILE: src/fw_fanctrl/dto/command_result/PrintFanSpeedCommandResult.py
================================================
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class PrintFanSpeedCommandResult(CommandResult):
def __init__(self, speed):
super().__init__(CommandStatus.SUCCESS)
self.speed = speed
def __str__(self):
return f"Current fan speed: '{self.speed}%'"
================================================
FILE: src/fw_fanctrl/dto/command_result/PrintStrategyListCommandResult.py
================================================
import os
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class PrintStrategyListCommandResult(CommandResult):
def __init__(self, strategies):
super().__init__(CommandStatus.SUCCESS)
self.strategies = strategies
def __str__(self):
printable_list = f"{os.linesep}- ".join(self.strategies)
return f"Strategy list: {os.linesep}- {printable_list}"
================================================
FILE: src/fw_fanctrl/dto/command_result/ServicePauseCommandResult.py
================================================
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class ServicePauseCommandResult(CommandResult):
def __init__(self):
super().__init__(CommandStatus.SUCCESS)
def __str__(self):
return "Service paused! The hardware fan control will take over"
================================================
FILE: src/fw_fanctrl/dto/command_result/ServiceResumeCommandResult.py
================================================
import os
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class ServiceResumeCommandResult(CommandResult):
def __init__(self, strategy, default):
super().__init__(CommandStatus.SUCCESS)
self.strategy = strategy
self.default = default
def __str__(self):
return (
f"Service resumed!{os.linesep}" f"Strategy in use: '{self.strategy}'{os.linesep}" f"Default: {self.default}"
)
================================================
FILE: src/fw_fanctrl/dto/command_result/SetConfigurationCommandResult.py
================================================
import json
import os
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class SetConfigurationCommandResult(CommandResult):
def __init__(self, strategy, default, configuration):
super().__init__(CommandStatus.SUCCESS)
self.strategy = strategy
self.configuration = configuration
self.default = default
def __str__(self):
return (
f"Configuration updated with success: {json.dumps(self.configuration)}.{os.linesep}"
f"Strategy in use: {self.strategy}{os.linesep}"
f"Default: {self.default}"
)
================================================
FILE: src/fw_fanctrl/dto/command_result/StrategyChangeCommandResult.py
================================================
import os
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class StrategyChangeCommandResult(CommandResult):
def __init__(self, strategy, default):
super().__init__(CommandStatus.SUCCESS)
self.strategy = strategy
self.default = default
def __str__(self):
return f"Strategy in use: '{self.strategy}'{os.linesep}" f"Default: {self.default}"
================================================
FILE: src/fw_fanctrl/dto/command_result/StrategyResetCommandResult.py
================================================
import os
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class StrategyResetCommandResult(CommandResult):
def __init__(self, strategy, default):
super().__init__(CommandStatus.SUCCESS)
self.strategy = strategy
self.default = default
def __str__(self):
return f"Strategy reset to default! Strategy in use: '{self.strategy}'{os.linesep}" f"Default: {self.default}"
================================================
FILE: src/fw_fanctrl/dto/command_result/__init__.py
================================================
================================================
FILE: src/fw_fanctrl/dto/runtime_result/RuntimeResult.py
================================================
from fw_fanctrl.dto.Printable import Printable
from fw_fanctrl.enum.CommandStatus import CommandStatus
class RuntimeResult(Printable):
def __init__(self, status, reason="Unexpected"):
super().__init__()
self.status = status
if status == CommandStatus.ERROR:
self.reason = reason
def __str__(self):
return "Success!" if self.status == CommandStatus.SUCCESS else f"[Error] > An error occurred: {self.reason}"
================================================
FILE: src/fw_fanctrl/dto/runtime_result/StatusRuntimeResult.py
================================================
import os
from fw_fanctrl.dto.runtime_result.RuntimeResult import RuntimeResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
class StatusRuntimeResult(RuntimeResult):
def __init__(
self,
strategy,
default,
speed,
temperature,
moving_average_temperature,
effective_temperature,
active,
configuration,
):
super().__init__(CommandStatus.SUCCESS)
self.strategy = strategy
self.default = default
self.speed = speed
self.temperature = temperature
self.movingAverageTemperature = moving_average_temperature
self.effectiveTemperature = effective_temperature
self.active = active
self.configuration = configuration
def __str__(self):
return (
f"Strategy: '{self.strategy}'{os.linesep}"
f"Default: {self.default}{os.linesep}"
f"Speed: {self.speed}%{os.linesep}"
f"Temp: {self.temperature}°C{os.linesep}"
f"MovingAverageTemp: {self.movingAverageTemperature}°C{os.linesep}"
f"EffectiveTemp: {self.effectiveTemperature}°C{os.linesep}"
f"Active: {self.active}{os.linesep}"
f"DefaultStrategy: '{self.configuration["data"]["defaultStrategy"]}'{os.linesep}"
f"DischargingStrategy: '{self.configuration["data"]["strategyOnDischarging"]}'{os.linesep}"
)
================================================
FILE: src/fw_fanctrl/dto/runtime_result/__init__.py
================================================
================================================
FILE: src/fw_fanctrl/enum/CommandStatus.py
================================================
from enum import Enum
class CommandStatus(str, Enum):
SUCCESS = "success"
ERROR = "error"
================================================
FILE: src/fw_fanctrl/enum/OutputFormat.py
================================================
from enum import Enum
class OutputFormat(str, Enum):
NATURAL = "NATURAL"
JSON = "JSON"
================================================
FILE: src/fw_fanctrl/enum/__init__.py
================================================
================================================
FILE: src/fw_fanctrl/exception/ConfigurationParsingException.py
================================================
class ConfigurationParsingException(Exception):
pass
================================================
FILE: src/fw_fanctrl/exception/InvalidStrategyException.py
================================================
class InvalidStrategyException(Exception):
pass
================================================
FILE: src/fw_fanctrl/exception/SocketAlreadyRunningException.py
================================================
class SocketAlreadyRunningException(Exception):
pass
================================================
FILE: src/fw_fanctrl/exception/SocketCallException.py
================================================
class SocketCallException(Exception):
pass
================================================
FILE: src/fw_fanctrl/exception/UnimplementedException.py
================================================
class UnimplementedException(Exception):
pass
================================================
FILE: src/fw_fanctrl/exception/UnknownCommandException.py
================================================
class UnknownCommandException(Exception):
pass
================================================
FILE: src/fw_fanctrl/exception/__init__.py
================================================
================================================
FILE: src/fw_fanctrl/hardwareController/EctoolHardwareController.py
================================================
import re
import subprocess
from abc import ABC
from fw_fanctrl.hardwareController.HardwareController import HardwareController
class EctoolHardwareController(HardwareController, ABC):
noBatterySensorMode = False
nonBatterySensors = None
def __init__(self, no_battery_sensor_mode=False):
if no_battery_sensor_mode:
self.noBatterySensorMode = True
self.populate_non_battery_sensors()
def populate_non_battery_sensors(self):
self.nonBatterySensors = []
raw_out = subprocess.run(
"ectool tempsinfo all",
stdout=subprocess.PIPE,
shell=True,
text=True,
).stdout
battery_sensors_raw = re.findall(r"\d+ Battery", raw_out, re.MULTILINE)
battery_sensors = [x.split(" ")[0] for x in battery_sensors_raw]
for x in re.findall(r"^\d+", raw_out, re.MULTILINE):
if x not in battery_sensors:
self.nonBatterySensors.append(x)
def get_temperature(self):
if self.noBatterySensorMode:
raw_out = "".join(
[
subprocess.run(
"ectool temps " + x,
stdout=subprocess.PIPE,
shell=True,
text=True,
).stdout
for x in self.nonBatterySensors
]
)
else:
raw_out = subprocess.run(
"ectool temps all",
stdout=subprocess.PIPE,
shell=True,
text=True,
).stdout
raw_temps = re.findall(r"\(= (\d+) C\)", raw_out)
temps = sorted([x for x in [int(x) for x in raw_temps] if x > 0], reverse=True)
# safety fallback to avoid damaging hardware
if len(temps) == 0:
return 50
return float(round(temps[0], 2))
def set_speed(self, speed):
subprocess.run(f"ectool fanduty {speed}", stdout=subprocess.PIPE, shell=True)
def is_on_ac(self):
raw_out = subprocess.run(
"ectool battery",
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
shell=True,
text=True,
).stdout
return len(re.findall(r"Flags.*(AC_PRESENT)", raw_out)) > 0
def pause(self):
subprocess.run("ectool autofanctrl", stdout=subprocess.PIPE, shell=True)
def resume(self):
# Empty for ectool, as setting an arbitrary speed disables the automatic fan control
pass
================================================
FILE: src/fw_fanctrl/hardwareController/HardwareController.py
================================================
from abc import ABC, abstractmethod
from fw_fanctrl.exception.UnimplementedException import UnimplementedException
class HardwareController(ABC):
@abstractmethod
def get_temperature(self):
raise UnimplementedException()
@abstractmethod
def set_speed(self, speed):
raise UnimplementedException()
@abstractmethod
def pause(self):
pass
@abstractmethod
def resume(self):
pass
@abstractmethod
def is_on_ac(self):
raise UnimplementedException()
================================================
FILE: src/fw_fanctrl/hardwareController/__init__.py
================================================
================================================
FILE: src/fw_fanctrl/socketController/SocketController.py
================================================
from abc import ABC, abstractmethod
from fw_fanctrl.exception.UnimplementedException import UnimplementedException
class SocketController(ABC):
@abstractmethod
def start_server_socket(self, command_callback=None):
raise UnimplementedException()
@abstractmethod
def stop_server_socket(self):
raise UnimplementedException()
@abstractmethod
def is_server_socket_running(self):
raise UnimplementedException()
@abstractmethod
def send_via_client_socket(self, command):
raise UnimplementedException()
================================================
FILE: src/fw_fanctrl/socketController/UnixSocketController.py
================================================
import io
import os
import shlex
import socket
import sys
from abc import ABC
from fw_fanctrl import COMMANDS_SOCKET_FILE_PATH, SOCKETS_FOLDER_PATH
from fw_fanctrl.CommandParser import CommandParser
from fw_fanctrl.dto.command_result.CommandResult import CommandResult
from fw_fanctrl.enum.CommandStatus import CommandStatus
from fw_fanctrl.enum.OutputFormat import OutputFormat
from fw_fanctrl.exception.SocketAlreadyRunningException import SocketAlreadyRunningException
from fw_fanctrl.exception.SocketCallException import SocketCallException
from fw_fanctrl.socketController.SocketController import SocketController
class UnixSocketController(SocketController, ABC):
server_socket = None
def start_server_socket(self, command_callback=None):
if self.server_socket:
raise SocketAlreadyRunningException(self.server_socket)
self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
if os.path.exists(COMMANDS_SOCKET_FILE_PATH):
os.remove(COMMANDS_SOCKET_FILE_PATH)
try:
if not os.path.exists(SOCKETS_FOLDER_PATH):
os.makedirs(SOCKETS_FOLDER_PATH)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind(COMMANDS_SOCKET_FILE_PATH)
os.chmod(COMMANDS_SOCKET_FILE_PATH, 0o777)
self.server_socket.listen(1)
while True:
client_socket, _ = self.server_socket.accept()
parse_print_capture = io.StringIO()
args = None
try:
# Receive data from the client
data = client_socket.recv(4096).decode()
original_stderr = sys.stderr
original_stdout = sys.stdout
# capture parsing std outputs for the client
sys.stderr = parse_print_capture
sys.stdout = parse_print_capture
try:
args = CommandParser(True).parse_args(shlex.split(data))
finally:
sys.stderr = original_stderr
sys.stdout = original_stdout
command_result = command_callback(args)
if args.output_format == OutputFormat.JSON:
if parse_print_capture.getvalue().strip():
command_result.info = parse_print_capture.getvalue()
client_socket.sendall(command_result.to_output_format(args.output_format).encode("utf-8"))
else:
natural_result = command_result.to_output_format(args.output_format)
if parse_print_capture.getvalue().strip():
natural_result = parse_print_capture.getvalue() + natural_result
client_socket.sendall(natural_result.encode("utf-8"))
except (SystemExit, Exception) as e:
_cre = CommandResult(
CommandStatus.ERROR, f"An error occurred while treating a socket command: {e}"
).to_output_format(getattr(args, "output_format", None))
print(_cre, file=sys.stderr)
client_socket.sendall(_cre.encode("utf-8"))
finally:
client_socket.shutdown(socket.SHUT_WR)
client_socket.close()
finally:
self.stop_server_socket()
def stop_server_socket(self):
if self.server_socket:
self.server_socket.close()
self.server_socket = None
def is_server_socket_running(self):
return self.server_socket is not None
def send_via_client_socket(self, command):
client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
client_socket.connect(COMMANDS_SOCKET_FILE_PATH)
client_socket.sendall(command.encode("utf-8"))
received_data = b""
while True:
data_chunk = client_socket.recv(1024)
if not data_chunk:
break
received_data += data_chunk
# Receive data from the server
data = received_data.decode()
if data.startswith("[Error] > "):
raise SocketCallException(data)
return data
finally:
if client_socket:
client_socket.close()
================================================
FILE: src/fw_fanctrl/socketController/__init__.py
================================================
gitextract_s4i5ocno/
├── .editorconfig
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report---packaging-aur.md
│ │ ├── bug-report---packaging-nix.md
│ │ ├── bug-report---packaging-windows.md
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── other.md
│ └── workflows/
│ ├── auto-tag-and-release.yml
│ └── pr-check-formatting.yml
├── .gitignore
├── LICENSE
├── README.md
├── doc/
│ ├── README.md
│ ├── commands.md
│ ├── configuration.md
│ └── nixos.md
├── fetch/
│ └── ectool/
│ ├── LICENSE
│ └── linux/
│ ├── gitlab_job_id
│ └── hash.sha256
├── install.sh
├── post-install.sh
├── pre-uninstall.sh
├── pyproject.toml
├── services/
│ ├── fw-fanctrl.service
│ └── system-sleep/
│ └── fw-fanctrl-suspend
└── src/
└── fw_fanctrl/
├── CommandParser.py
├── Configuration.py
├── FanController.py
├── Strategy.py
├── __init__.py
├── __main__.py
├── _resources/
│ ├── config.json
│ └── config.schema.json
├── dto/
│ ├── Printable.py
│ ├── __init__.py
│ ├── command_result/
│ │ ├── CommandResult.py
│ │ ├── ConfigurationReloadCommandResult.py
│ │ ├── PrintActiveCommandResult.py
│ │ ├── PrintCurrentStrategyCommandResult.py
│ │ ├── PrintFanSpeedCommandResult.py
│ │ ├── PrintStrategyListCommandResult.py
│ │ ├── ServicePauseCommandResult.py
│ │ ├── ServiceResumeCommandResult.py
│ │ ├── SetConfigurationCommandResult.py
│ │ ├── StrategyChangeCommandResult.py
│ │ ├── StrategyResetCommandResult.py
│ │ └── __init__.py
│ └── runtime_result/
│ ├── RuntimeResult.py
│ ├── StatusRuntimeResult.py
│ └── __init__.py
├── enum/
│ ├── CommandStatus.py
│ ├── OutputFormat.py
│ └── __init__.py
├── exception/
│ ├── ConfigurationParsingException.py
│ ├── InvalidStrategyException.py
│ ├── SocketAlreadyRunningException.py
│ ├── SocketCallException.py
│ ├── UnimplementedException.py
│ ├── UnknownCommandException.py
│ └── __init__.py
├── hardwareController/
│ ├── EctoolHardwareController.py
│ ├── HardwareController.py
│ └── __init__.py
└── socketController/
├── SocketController.py
├── UnixSocketController.py
└── __init__.py
SYMBOL INDEX (108 symbols across 31 files)
FILE: src/fw_fanctrl/CommandParser.py
class CommandParser (line 11) | class CommandParser:
method __init__ (line 17) | def __init__(self, is_remote=False):
method init_parser (line 22) | def init_parser(self):
method init_legacy_parser (line 122) | def init_legacy_parser(self):
method parse_args (line 169) | def parse_args(self, args=None):
FILE: src/fw_fanctrl/Configuration.py
class Configuration (line 17) | class Configuration:
method __init__ (line 21) | def __init__(self, path):
method parse (line 25) | def parse(self, raw_config):
method reload (line 44) | def reload(self):
method save (line 51) | def save(self):
method get_strategies (line 56) | def get_strategies(self):
method get_strategy (line 59) | def get_strategy(self, strategy_name):
method get_default_strategy (line 70) | def get_default_strategy(self):
method get_discharging_strategy (line 73) | def get_discharging_strategy(self):
FILE: src/fw_fanctrl/FanController.py
class FanController (line 24) | class FanController:
method __init__ (line 35) | def __init__(self, hardware_controller, socket_controller, config_path...
method get_actual_temperature (line 52) | def get_actual_temperature(self):
method set_speed (line 55) | def set_speed(self, speed):
method is_on_ac (line 59) | def is_on_ac(self):
method pause (line 62) | def pause(self):
method resume (line 66) | def resume(self):
method overwrite_strategy (line 70) | def overwrite_strategy(self, strategy_name):
method clear_overwritten_strategy (line 77) | def clear_overwritten_strategy(self):
method get_current_strategy (line 81) | def get_current_strategy(self):
method command_manager (line 88) | def command_manager(self, args):
method get_moving_average_temperature (line 132) | def get_moving_average_temperature(self, time_interval):
method get_effective_temperature (line 138) | def get_effective_temperature(self, current_temp, time_interval):
method adapt_speed (line 142) | def adapt_speed(self, current_temp):
method dump_details (line 162) | def dump_details(self):
method print_state (line 179) | def print_state(self):
method run (line 182) | def run(self, debug=True):
FILE: src/fw_fanctrl/Strategy.py
class Strategy (line 1) | class Strategy:
method __init__ (line 7) | def __init__(self, name, parameters):
FILE: src/fw_fanctrl/__main__.py
function main (line 13) | def main():
FILE: src/fw_fanctrl/dto/Printable.py
class Printable (line 6) | class Printable:
method __init__ (line 7) | def __init__(self):
method to_output_format (line 10) | def to_output_format(self, output_format):
FILE: src/fw_fanctrl/dto/command_result/CommandResult.py
class CommandResult (line 5) | class CommandResult(Printable):
method __init__ (line 6) | def __init__(self, status, reason="Unexpected"):
method __str__ (line 12) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/ConfigurationReloadCommandResult.py
class ConfigurationReloadCommandResult (line 7) | class ConfigurationReloadCommandResult(CommandResult):
method __init__ (line 8) | def __init__(self, strategy, default):
method __str__ (line 13) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/PrintActiveCommandResult.py
class PrintActiveCommandResult (line 5) | class PrintActiveCommandResult(CommandResult):
method __init__ (line 6) | def __init__(self, active):
method __str__ (line 10) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/PrintCurrentStrategyCommandResult.py
class PrintCurrentStrategyCommandResult (line 7) | class PrintCurrentStrategyCommandResult(CommandResult):
method __init__ (line 8) | def __init__(self, strategy, default):
method __str__ (line 13) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/PrintFanSpeedCommandResult.py
class PrintFanSpeedCommandResult (line 5) | class PrintFanSpeedCommandResult(CommandResult):
method __init__ (line 6) | def __init__(self, speed):
method __str__ (line 10) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/PrintStrategyListCommandResult.py
class PrintStrategyListCommandResult (line 7) | class PrintStrategyListCommandResult(CommandResult):
method __init__ (line 8) | def __init__(self, strategies):
method __str__ (line 12) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/ServicePauseCommandResult.py
class ServicePauseCommandResult (line 5) | class ServicePauseCommandResult(CommandResult):
method __init__ (line 6) | def __init__(self):
method __str__ (line 9) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/ServiceResumeCommandResult.py
class ServiceResumeCommandResult (line 7) | class ServiceResumeCommandResult(CommandResult):
method __init__ (line 8) | def __init__(self, strategy, default):
method __str__ (line 13) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/SetConfigurationCommandResult.py
class SetConfigurationCommandResult (line 8) | class SetConfigurationCommandResult(CommandResult):
method __init__ (line 9) | def __init__(self, strategy, default, configuration):
method __str__ (line 15) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/StrategyChangeCommandResult.py
class StrategyChangeCommandResult (line 7) | class StrategyChangeCommandResult(CommandResult):
method __init__ (line 8) | def __init__(self, strategy, default):
method __str__ (line 13) | def __str__(self):
FILE: src/fw_fanctrl/dto/command_result/StrategyResetCommandResult.py
class StrategyResetCommandResult (line 7) | class StrategyResetCommandResult(CommandResult):
method __init__ (line 8) | def __init__(self, strategy, default):
method __str__ (line 13) | def __str__(self):
FILE: src/fw_fanctrl/dto/runtime_result/RuntimeResult.py
class RuntimeResult (line 5) | class RuntimeResult(Printable):
method __init__ (line 6) | def __init__(self, status, reason="Unexpected"):
method __str__ (line 12) | def __str__(self):
FILE: src/fw_fanctrl/dto/runtime_result/StatusRuntimeResult.py
class StatusRuntimeResult (line 7) | class StatusRuntimeResult(RuntimeResult):
method __init__ (line 8) | def __init__(
method __str__ (line 29) | def __str__(self):
FILE: src/fw_fanctrl/enum/CommandStatus.py
class CommandStatus (line 4) | class CommandStatus(str, Enum):
FILE: src/fw_fanctrl/enum/OutputFormat.py
class OutputFormat (line 4) | class OutputFormat(str, Enum):
FILE: src/fw_fanctrl/exception/ConfigurationParsingException.py
class ConfigurationParsingException (line 1) | class ConfigurationParsingException(Exception):
FILE: src/fw_fanctrl/exception/InvalidStrategyException.py
class InvalidStrategyException (line 1) | class InvalidStrategyException(Exception):
FILE: src/fw_fanctrl/exception/SocketAlreadyRunningException.py
class SocketAlreadyRunningException (line 1) | class SocketAlreadyRunningException(Exception):
FILE: src/fw_fanctrl/exception/SocketCallException.py
class SocketCallException (line 1) | class SocketCallException(Exception):
FILE: src/fw_fanctrl/exception/UnimplementedException.py
class UnimplementedException (line 1) | class UnimplementedException(Exception):
FILE: src/fw_fanctrl/exception/UnknownCommandException.py
class UnknownCommandException (line 1) | class UnknownCommandException(Exception):
FILE: src/fw_fanctrl/hardwareController/EctoolHardwareController.py
class EctoolHardwareController (line 8) | class EctoolHardwareController(HardwareController, ABC):
method __init__ (line 12) | def __init__(self, no_battery_sensor_mode=False):
method populate_non_battery_sensors (line 17) | def populate_non_battery_sensors(self):
method get_temperature (line 31) | def get_temperature(self):
method set_speed (line 58) | def set_speed(self, speed):
method is_on_ac (line 61) | def is_on_ac(self):
method pause (line 71) | def pause(self):
method resume (line 74) | def resume(self):
FILE: src/fw_fanctrl/hardwareController/HardwareController.py
class HardwareController (line 6) | class HardwareController(ABC):
method get_temperature (line 8) | def get_temperature(self):
method set_speed (line 12) | def set_speed(self, speed):
method pause (line 16) | def pause(self):
method resume (line 20) | def resume(self):
method is_on_ac (line 24) | def is_on_ac(self):
FILE: src/fw_fanctrl/socketController/SocketController.py
class SocketController (line 6) | class SocketController(ABC):
method start_server_socket (line 8) | def start_server_socket(self, command_callback=None):
method stop_server_socket (line 12) | def stop_server_socket(self):
method is_server_socket_running (line 16) | def is_server_socket_running(self):
method send_via_client_socket (line 20) | def send_via_client_socket(self, command):
FILE: src/fw_fanctrl/socketController/UnixSocketController.py
class UnixSocketController (line 18) | class UnixSocketController(SocketController, ABC):
method start_server_socket (line 21) | def start_server_socket(self, command_callback=None):
method stop_server_socket (line 76) | def stop_server_socket(self):
method is_server_socket_running (line 81) | def is_server_socket_running(self):
method send_via_client_socket (line 84) | def send_via_client_socket(self, command):
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (116K chars).
[
{
"path": ".editorconfig",
"chars": 211,
"preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 4\nmax_line_length = 120\ncharset = utf-8\ntrim_trailing_whitespace = t"
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report---packaging-aur.md",
"chars": 881,
"preview": "---\nname: Bug report | packaging/AUR\nabout: Report a bug involving the AUR packaging\ntitle: \"[BUG] [packaging/AUR]\"\nlabe"
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report---packaging-nix.md",
"chars": 917,
"preview": "---\nname: Bug report | packaging/nix\nabout: Report a bug involving the nix packaging\ntitle: \"[BUG] [packaging/nix]\"\nlabe"
},
{
"path": ".github/ISSUE_TEMPLATE/bug-report---packaging-windows.md",
"chars": 849,
"preview": "---\nname: Bug report | packaging/windows\nabout: Report a bug involving the windows packaging\ntitle: \"[BUG] [packaging/wi"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 822,
"preview": "---\nname: Bug report\nabout: Report a bug in the program\ntitle: \"[BUG] \"\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 677,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[FEATURE]\"\nlabels: enhancement\nassignees: ''\n\n"
},
{
"path": ".github/ISSUE_TEMPLATE/other.md",
"chars": 90,
"preview": "---\nname: Other\nabout: Any other issue/question\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n"
},
{
"path": ".github/workflows/auto-tag-and-release.yml",
"chars": 2483,
"preview": "name: Auto Tag and Release on Version Change\n\non:\n push:\n branches:\n - main\n\njobs:\n tag:\n name: Tag if Vers"
},
{
"path": ".github/workflows/pr-check-formatting.yml",
"chars": 593,
"preview": "name: Check Formatting on PR\n\non:\n pull_request:\n types:\n - synchronize\n branches:\n - main\n\njobs:\n che"
},
{
"path": ".gitignore",
"chars": 5062,
"preview": "### Python template\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Di"
},
{
"path": "LICENSE",
"chars": 1518,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2022, TamTamHero\nAll rights reserved.\n\nRedistribution and use in source and binary f"
},
{
"path": "README.md",
"chars": 16161,
"preview": "# fw-fanctrl\n\n[\n- [Development Setup](../README.md#development-s"
},
{
"path": "doc/commands.md",
"chars": 3128,
"preview": "# Commands\n\nHere is a list of commands and options used to interact with the service.\n\nthe base of all commands is the f"
},
{
"path": "doc/configuration.md",
"chars": 4118,
"preview": "# Table of Content\n\n<!-- TOC -->\n* [Table of Content](#table-of-content)\n* [Configuration](#configuration)\n * [Default "
},
{
"path": "doc/nixos.md",
"chars": 1892,
"preview": "# Module and Package\nFor [NixOS](https://nixos.org/) verion >25.05 this package (derivation) is in the offical [Nixpkgs]"
},
{
"path": "fetch/ectool/LICENSE",
"chars": 1561,
"preview": "// Copyright 2010 The Chromium OS Authors. All rights reserved.\n//\n// Redistribution and use in source and binary forms,"
},
{
"path": "fetch/ectool/linux/gitlab_job_id",
"chars": 3,
"preview": "899"
},
{
"path": "fetch/ectool/linux/hash.sha256",
"chars": 64,
"preview": "ab94a1e9a33f592d5482dbfd4f42ad351ef91227ee3b3707333c0107d7f2b1b0"
},
{
"path": "install.sh",
"chars": 12326,
"preview": "#!/bin/bash\nset -e\n\n# Argument parsing\nSHORT=r,d:,p:,s:,h\nLONG=remove,dest-dir:,prefix-dir:,sysconf-dir:,no-ectool,no-pr"
},
{
"path": "post-install.sh",
"chars": 1954,
"preview": "#!/bin/bash\nset -e\n\nHOME_DIR=\"$(eval echo \"~$(logname)\")\"\n\n# Argument parsing\nNO_SUDO=false\nSHORT=d:,s:,h\nLONG=dest-dir:"
},
{
"path": "pre-uninstall.sh",
"chars": 1304,
"preview": "#!/bin/bash\nset -e\n\n# Argument parsing\nNO_SUDO=false\nSHORT=h\nLONG=no-sudo,help\nVALID_ARGS=$(getopt -a --options $SHORT -"
},
{
"path": "pyproject.toml",
"chars": 1478,
"preview": "[tool.black]\nline-length = 120\nexclude = '''\n/(\n \\.git\n | \\.github\n | \\.idea\n | \\.vscode\n | \\.hg\n | \\.mypy_cache"
},
{
"path": "services/fw-fanctrl.service",
"chars": 350,
"preview": "[Unit]\nDescription=Framework Fan Controller\nAfter=multi-user.target\n[Service]\nType=simple\nRestart=always\nExecStart=\"%PYT"
},
{
"path": "services/system-sleep/fw-fanctrl-suspend",
"chars": 138,
"preview": "#!/bin/sh\n\ncase $1 in\n pre) \"%PYTHON_SCRIPT_INSTALLATION_PATH%\" pause ;;\n post) \"%PYTHON_SCRIPT_INSTALLATION_PATH"
},
{
"path": "src/fw_fanctrl/CommandParser.py",
"chars": 8864,
"preview": "import argparse\nimport os\nimport sys\nimport textwrap\n\nfrom fw_fanctrl import DEFAULT_CONFIGURATION_FILE_PATH\nfrom fw_fan"
},
{
"path": "src/fw_fanctrl/Configuration.py",
"chars": 2932,
"preview": "import json\nfrom json import JSONDecodeError\nfrom os.path import isfile\nfrom shutil import copyfile\n\nimport jsonschema\n\n"
},
{
"path": "src/fw_fanctrl/FanController.py",
"chars": 9530,
"preview": "import collections\nimport sys\nimport threading\nfrom time import sleep\n\nfrom fw_fanctrl.Configuration import Configuratio"
},
{
"path": "src/fw_fanctrl/Strategy.py",
"chars": 677,
"preview": "class Strategy:\n name = None\n fan_speed_update_frequency = None\n moving_average_interval = None\n speed_curve"
},
{
"path": "src/fw_fanctrl/__init__.py",
"chars": 323,
"preview": "import importlib.resources\nimport os\n\nINTERNAL_RESOURCES_PATH = importlib.resources.files(\"fw_fanctrl\").joinpath(\"_resou"
},
{
"path": "src/fw_fanctrl/__main__.py",
"chars": 2072,
"preview": "import shlex\nimport sys\n\nfrom fw_fanctrl.CommandParser import CommandParser\nfrom fw_fanctrl.FanController import FanCont"
},
{
"path": "src/fw_fanctrl/_resources/config.json",
"chars": 2878,
"preview": "{\n \"$schema\": \"./config.schema.json\",\n \"defaultStrategy\": \"lazy\",\n \"strategyOnDischarging\" : \"\",\n \"strategie"
},
{
"path": "src/fw_fanctrl/_resources/config.schema.json",
"chars": 5022,
"preview": "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"file://config.schema.json\",\n \"title\": \"f"
},
{
"path": "src/fw_fanctrl/dto/Printable.py",
"chars": 320,
"preview": "import json\n\nfrom fw_fanctrl.enum.OutputFormat import OutputFormat\n\n\nclass Printable:\n def __init__(self):\n su"
},
{
"path": "src/fw_fanctrl/dto/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/fw_fanctrl/dto/command_result/CommandResult.py",
"chars": 462,
"preview": "from fw_fanctrl.dto.Printable import Printable\nfrom fw_fanctrl.enum.CommandStatus import CommandStatus\n\n\nclass CommandRe"
},
{
"path": "src/fw_fanctrl/dto/command_result/ConfigurationReloadCommandResult.py",
"chars": 488,
"preview": "import os\n\nfrom fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus impo"
},
{
"path": "src/fw_fanctrl/dto/command_result/PrintActiveCommandResult.py",
"chars": 348,
"preview": "from fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus import CommandS"
},
{
"path": "src/fw_fanctrl/dto/command_result/PrintCurrentStrategyCommandResult.py",
"chars": 466,
"preview": "import os\n\nfrom fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus impo"
},
{
"path": "src/fw_fanctrl/dto/command_result/PrintFanSpeedCommandResult.py",
"chars": 360,
"preview": "from fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus import CommandS"
},
{
"path": "src/fw_fanctrl/dto/command_result/PrintStrategyListCommandResult.py",
"chars": 466,
"preview": "import os\n\nfrom fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus impo"
},
{
"path": "src/fw_fanctrl/dto/command_result/ServicePauseCommandResult.py",
"chars": 345,
"preview": "from fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus import CommandS"
},
{
"path": "src/fw_fanctrl/dto/command_result/ServiceResumeCommandResult.py",
"chars": 515,
"preview": "import os\n\nfrom fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus impo"
},
{
"path": "src/fw_fanctrl/dto/command_result/SetConfigurationCommandResult.py",
"chars": 663,
"preview": "import json\nimport os\n\nfrom fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.Comman"
},
{
"path": "src/fw_fanctrl/dto/command_result/StrategyChangeCommandResult.py",
"chars": 460,
"preview": "import os\n\nfrom fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus impo"
},
{
"path": "src/fw_fanctrl/dto/command_result/StrategyResetCommandResult.py",
"chars": 486,
"preview": "import os\n\nfrom fw_fanctrl.dto.command_result.CommandResult import CommandResult\nfrom fw_fanctrl.enum.CommandStatus impo"
},
{
"path": "src/fw_fanctrl/dto/command_result/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/fw_fanctrl/dto/runtime_result/RuntimeResult.py",
"chars": 462,
"preview": "from fw_fanctrl.dto.Printable import Printable\nfrom fw_fanctrl.enum.CommandStatus import CommandStatus\n\n\nclass RuntimeRe"
},
{
"path": "src/fw_fanctrl/dto/runtime_result/StatusRuntimeResult.py",
"chars": 1430,
"preview": "import os\n\nfrom fw_fanctrl.dto.runtime_result.RuntimeResult import RuntimeResult\nfrom fw_fanctrl.enum.CommandStatus impo"
},
{
"path": "src/fw_fanctrl/dto/runtime_result/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/fw_fanctrl/enum/CommandStatus.py",
"chars": 100,
"preview": "from enum import Enum\n\n\nclass CommandStatus(str, Enum):\n SUCCESS = \"success\"\n ERROR = \"error\"\n"
},
{
"path": "src/fw_fanctrl/enum/OutputFormat.py",
"chars": 97,
"preview": "from enum import Enum\n\n\nclass OutputFormat(str, Enum):\n NATURAL = \"NATURAL\"\n JSON = \"JSON\"\n"
},
{
"path": "src/fw_fanctrl/enum/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/fw_fanctrl/exception/ConfigurationParsingException.py",
"chars": 57,
"preview": "class ConfigurationParsingException(Exception):\n pass\n"
},
{
"path": "src/fw_fanctrl/exception/InvalidStrategyException.py",
"chars": 52,
"preview": "class InvalidStrategyException(Exception):\n pass\n"
},
{
"path": "src/fw_fanctrl/exception/SocketAlreadyRunningException.py",
"chars": 57,
"preview": "class SocketAlreadyRunningException(Exception):\n pass\n"
},
{
"path": "src/fw_fanctrl/exception/SocketCallException.py",
"chars": 47,
"preview": "class SocketCallException(Exception):\n pass\n"
},
{
"path": "src/fw_fanctrl/exception/UnimplementedException.py",
"chars": 50,
"preview": "class UnimplementedException(Exception):\n pass\n"
},
{
"path": "src/fw_fanctrl/exception/UnknownCommandException.py",
"chars": 51,
"preview": "class UnknownCommandException(Exception):\n pass\n"
},
{
"path": "src/fw_fanctrl/exception/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/fw_fanctrl/hardwareController/EctoolHardwareController.py",
"chars": 2558,
"preview": "import re\nimport subprocess\nfrom abc import ABC\n\nfrom fw_fanctrl.hardwareController.HardwareController import HardwareCo"
},
{
"path": "src/fw_fanctrl/hardwareController/HardwareController.py",
"chars": 526,
"preview": "from abc import ABC, abstractmethod\n\nfrom fw_fanctrl.exception.UnimplementedException import UnimplementedException\n\n\ncl"
},
{
"path": "src/fw_fanctrl/hardwareController/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/fw_fanctrl/socketController/SocketController.py",
"chars": 565,
"preview": "from abc import ABC, abstractmethod\n\nfrom fw_fanctrl.exception.UnimplementedException import UnimplementedException\n\n\ncl"
},
{
"path": "src/fw_fanctrl/socketController/UnixSocketController.py",
"chars": 4554,
"preview": "import io\nimport os\nimport shlex\nimport socket\nimport sys\nfrom abc import ABC\n\nfrom fw_fanctrl import COMMANDS_SOCKET_FI"
},
{
"path": "src/fw_fanctrl/socketController/__init__.py",
"chars": 0,
"preview": ""
}
]
About this extraction
This page contains the full source code of the TamtamHero/fw-fanctrl GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (104.6 KB), approximately 25.6k tokens, and a symbol index with 108 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.