Full Code of TamtamHero/fw-fanctrl for AI

main 776f619cea2b cached
66 files
104.6 KB
25.6k tokens
108 symbols
1 requests
Download .txt
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

[![Static Badge](https://img.shields.io/badge/Linux%E2%80%AF%2F%E2%80%AFGlobal-FCC624?style=flat&logo=linux&logoColor=FFFFFF&label=Platform&link=https%3A%2F%2Fgithub.com%2FTamtamHero%2Ffw-fanctrl%2Ftree%2Fmain)](https://github.com/TamtamHero/fw-fanctrl/tree/main)
![Static Badge](https://img.shields.io/badge/no%20binary%20blobs-30363D?style=flat&logo=GitHub-Sponsors&logoColor=4dff61)

[![Static Badge](https://img.shields.io/badge/Python%203.12-FFDE57?style=flat&label=Requirement&link=https%3A%2F%2Fwww.python.org%2Fdownloads)](https://www.python.org/downloads)

## Platforms

[![Static Badge](https://img.shields.io/badge/Linux%E2%80%AF%2F%E2%80%AFGlobal-FCC624?style=flat&logo=linux&logoColor=FFFFFF&label=Platform&link=https%3A%2F%2Fgithub.com%2FTamtamHero%2Ffw-fanctrl%2Ftree%2Fmain)](https://github.com/TamtamHero/fw-fanctrl/tree/main)
[![Static Badge](https://img.shields.io/badge/NixOS-5277C3?style=flat&logo=nixos&logoColor=FFFFFF&label=Platform&link=https%3A%2F%2Fgithub.com%2FTamtamHero%2Ffw-fanctrl%2Ftree%2Fpackaging%2Fnix)](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix/doc/nix-flake.md)

**Third-party**<br>

[![Static Badge](https://img.shields.io/badge/Arch%20Linux-1793D1?style=flat&logo=archlinux&logoColor=FFFFFF&label=Platform&link=https%3A%2F%2Faur.archlinux.org%2Fpackages%2Ffw-fanctrl-git)](https://aur.archlinux.org/packages/fw-fanctrl-git)
[![Static Badge](https://img.shields.io/badge/Fedora-51A2DA?style=flat&logo=fedora&logoColor=FFFFFF&label=Platform&link=https%3A%2F%2Fgithub.com%2Ftulilirockz%2Ffw-fanctrl-rpm)](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&#8209;fanctrl&#8209;gui](https://github.com/leopoldhub/fw-fanctrl-gui)                                        | Simple customtkinter python gui with system tray for fw&#8209;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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Package&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Branch&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Documentation                                                                                                     |
|------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| Linux&nbsp;/&nbsp;Global                                                                                         | [installation&nbsp;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&#8209;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&#8209;flake](https://github.com/TamtamHero/fw-fanctrl/tree/main/doc/nixos.md)|

**Third-party**

| Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Package&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Documentation                                                        |
|------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------|
| Arch&nbsp;Linux                                                                                                  | [AUR](https://aur.archlinux.org/packages/fw-fanctrl-git)                                                            |                                                                      |
| Fedora&nbsp;/&nbsp;RPM                                                                                           | [COPR](https://copr.fedorainfracloud.org/coprs/tulilirockz/fw-fanctrl/package/fw-fanctrl/)                          | [GIT&nbsp;repository](https://github.com/tulilirockz/fw-fanctrl-rpm) |

### Requirements

| Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | 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&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Version&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Url &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; | Sub&#8209;dependencies | Exclusion&nbsp;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
================================================
Download .txt
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
Download .txt
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[![Static Badge](https://img.shields.io/badge/Linux%E2%80%AF%2F%E2%80%AFGlobal-FCC624?style=flat&logo=linu"
  },
  {
    "path": "doc/README.md",
    "chars": 286,
    "preview": "# Table of Content\n\n- [Default Installation](../README.md#installation)\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.

Copied to clipboard!