├── library ├── .coveragerc ├── MANIFEST.in ├── tox.ini ├── CHANGELOG.txt ├── LICENSE.txt ├── setup.py ├── tests │ ├── test_setup.py │ └── conftest.py ├── setup.cfg ├── README.md └── fanshim │ └── __init__.py ├── .stickler.yml ├── .gitignore ├── examples ├── led.py ├── led2.py ├── toggle.py ├── button.py ├── manual.py ├── README.md ├── automatic.py └── install-service.sh ├── uninstall.sh ├── .github └── workflows │ └── test.yml ├── LICENSE ├── Makefile ├── install.sh └── README.md /library/.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = fanshim 3 | omit = 4 | .tox/* 5 | -------------------------------------------------------------------------------- /.stickler.yml: -------------------------------------------------------------------------------- 1 | --- 2 | linters: 3 | flake8: 4 | python: 3 5 | max-line-length: 160 6 | -------------------------------------------------------------------------------- /library/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGELOG.txt 2 | include LICENSE.txt 3 | include README.md 4 | include setup.py 5 | recursive-include fanshim *.py 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | _build/ 3 | *.o 4 | *.so 5 | *.a 6 | *.py[cod] 7 | *.egg-info 8 | dist/ 9 | __pycache__ 10 | .DS_Store 11 | *.deb 12 | *.dsc 13 | *.build 14 | *.changes 15 | *.orig.* 16 | packaging/*tar.xz 17 | library/debian/ 18 | .coverage 19 | .pytest_cache 20 | .tox 21 | -------------------------------------------------------------------------------- /examples/led.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from fanshim import FanShim 3 | import time 4 | import colorsys 5 | 6 | fanshim = FanShim() 7 | fanshim.set_fan(False) 8 | 9 | try: 10 | while True: 11 | h = time.time() / 25.0 12 | r, g, b = [int(c * 255) for c in colorsys.hsv_to_rgb(h, 1.0, 1.0)] 13 | fanshim.set_light(r, g, b, brightness=0.05) 14 | time.sleep(1.0 / 60) 15 | 16 | except KeyboardInterrupt: 17 | pass 18 | -------------------------------------------------------------------------------- /library/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,35,37,39},qa 3 | skip_missing_interpreters = True 4 | 5 | [testenv] 6 | commands = 7 | python setup.py install 8 | coverage run -m py.test -v -r wsx 9 | coverage report 10 | deps = 11 | mock 12 | pytest>=3.1 13 | pytest-cov 14 | 15 | [testenv:qa] 16 | commands = 17 | check-manifest --ignore tox.ini,tests/*,.coveragerc 18 | python setup.py sdist bdist_wheel 19 | twine check dist/* 20 | flake8 --ignore E501 21 | deps = 22 | check-manifest 23 | flake8 24 | twine 25 | 26 | -------------------------------------------------------------------------------- /library/CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 0.0.5 2 | ----- 3 | 4 | * Replace Plasma API with APA102 library 5 | * Add support for setting LED global brightness 6 | * Add support for disabling button and/or LED 7 | * Move packages/requires to setup.config, minimum version now Python 2.7 8 | 9 | 0.0.4 10 | ----- 11 | 12 | * Prepare Fan SHIM to use legacy Plasma API 13 | 14 | 0.0.3 15 | ----- 16 | 17 | * Fix: lower polling frequency and make customisable, for PR #6 18 | 19 | 0.0.2 20 | ----- 21 | 22 | * Fix: Fix error on exit 23 | 24 | 0.0.1 25 | ----- 26 | 27 | * Initial Release 28 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | LIBRARY_VERSION=`cat library/setup.cfg | grep version | awk -F" = " '{print $2}'` 4 | LIBRARY_NAME=`cat library/setup.cfg | grep name | awk -F" = " '{print $2}'` 5 | 6 | printf "$LIBRARY_NAME $LIBRARY_VERSION Python Library: Uninstaller\n\n" 7 | 8 | if [ $(id -u) -ne 0 ]; then 9 | printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" 10 | exit 1 11 | fi 12 | 13 | cd library 14 | 15 | printf "Unnstalling for Python 2..\n" 16 | pip uninstall $LIBRARY_NAME 17 | 18 | if [ -f "/usr/bin/pip3" ]; then 19 | printf "Uninstalling for Python 3..\n" 20 | pip3 uninstall $LIBRARY_NAME 21 | fi 22 | 23 | cd .. 24 | 25 | printf "Done!\n" 26 | -------------------------------------------------------------------------------- /examples/led2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from fanshim import FanShim 3 | import time 4 | import colorsys 5 | 6 | fanshim = FanShim() 7 | fanshim.set_fan(False) 8 | 9 | try: 10 | for i in range(-10, 21): 11 | print("Temp index %d" % i) 12 | # Normal temperature range 0 to 1, cold < 0, hot > 1 13 | temp = i/10.0 14 | if temp < 0.0: 15 | # hue of blue through to green 16 | hue = (120.0 / 360.0) - (temp * 120.0 / 360.0) 17 | elif temp > 1.0: 18 | # hue of red to through to magenta 19 | hue = ((1.0 - temp) * 60.0 / 360.0) + 1.0 20 | else: 21 | # hue of green through to red 22 | hue = (1.0 - temp) * 120.0 / 360.0 23 | 24 | r, g, b = [int(c * 255.0) for c in colorsys.hsv_to_rgb(hue, 1.0, 1.0)] 25 | fanshim.set_light(r, g, b) 26 | 27 | time.sleep(1.0) 28 | 29 | except KeyboardInterrupt: 30 | pass 31 | -------------------------------------------------------------------------------- /examples/toggle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import signal 3 | from fanshim import FanShim 4 | 5 | """ 6 | This example attaches basic pressed, released and held handlers for Fan SHIM's button. 7 | 8 | If you want to use both a short press, and a long press you should use the "release" 9 | handler to perform the actual action. 10 | 11 | Since the "release" handler receives 1 argument - "was_held" - you don't need to bind 12 | the "held" handler at all if you're just doing a standard short/long press action. 13 | """ 14 | 15 | fanshim = FanShim() 16 | 17 | 18 | def update_led(state): 19 | if state: 20 | fanshim.set_light(0, 255, 0) 21 | else: 22 | fanshim.set_light(255, 0, 0) 23 | 24 | 25 | @fanshim.on_release() 26 | def release_handler(was_held): 27 | state = fanshim.toggle_fan() 28 | update_led(state) 29 | 30 | 31 | try: 32 | update_led(fanshim.get_fan()) 33 | signal.pause() 34 | except KeyboardInterrupt: 35 | pass 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Python Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python: [2.7, 3.5, 3.7, 3.9] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python }} 22 | - name: Install Dependencies 23 | run: | 24 | python -m pip install --upgrade setuptools tox 25 | - name: Run Tests 26 | working-directory: library 27 | run: | 28 | tox -e py 29 | - name: Coverage 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | working-directory: library 33 | run: | 34 | python -m pip install coveralls 35 | coveralls --service=github 36 | if: ${{ matrix.python == '3.9' }} 37 | 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /library/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pimoroni Ltd. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/button.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import signal 3 | from fanshim import FanShim 4 | 5 | """ 6 | This example attaches basic pressed, released and held handlers for Fan SHIM's button. 7 | 8 | If you want to use both a short press, and a long press you should use the "release" 9 | handler to perform the actual action. 10 | 11 | Since the "release" handler receives 1 argument - "was_held" - you don't need to bind 12 | the "held" handler at all if you're just doing a standard short/long press action. 13 | """ 14 | 15 | fanshim = FanShim() 16 | 17 | # Set the button hold time, in seconds 18 | fanshim.set_hold_time(1.0) 19 | 20 | 21 | @fanshim.on_press() 22 | def press_handler(): 23 | print("Pressed") 24 | 25 | 26 | @fanshim.on_release() 27 | def release_handler(was_held): 28 | print("Released") 29 | if was_held: 30 | print("Long press.") 31 | else: 32 | print("Short press.") 33 | 34 | 35 | # Not needed to detect short/long press 36 | # But included to demonstrate when it happens 37 | @fanshim.on_hold() 38 | def hold_handler(): 39 | print("HELD") 40 | 41 | 42 | try: 43 | signal.pause() 44 | except KeyboardInterrupt: 45 | pass 46 | -------------------------------------------------------------------------------- /library/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | Copyright (c) 2016 Pimoroni 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from setuptools import setup 26 | 27 | setup() 28 | -------------------------------------------------------------------------------- /library/tests/test_setup.py: -------------------------------------------------------------------------------- 1 | import mock 2 | import sys 3 | 4 | 5 | def test_setup(GPIO, apa102, FanShim): 6 | fanshim = FanShim() 7 | 8 | GPIO.setwarnings.assert_called_once_with(False) 9 | GPIO.setmode.assert_called_once_with(GPIO.BCM) 10 | GPIO.setup.assert_has_calls([ 11 | mock.call(fanshim._pin_fancontrol, GPIO.OUT), 12 | mock.call(fanshim._pin_button, GPIO.IN, pull_up_down=GPIO.PUD_UP) 13 | ]) 14 | 15 | apa102.APA102.assert_called_once_with(1, 15, 14, None, brightness=0.05) 16 | 17 | 18 | def test_button_disable(GPIO, apa102, FanShim): 19 | fanshim = FanShim(disable_button=True) 20 | 21 | GPIO.setwarnings.assert_called_once_with(False) 22 | GPIO.setmode.assert_called_once_with(GPIO.BCM) 23 | GPIO.setup.assert_called_once_with(fanshim._pin_fancontrol, GPIO.OUT) 24 | 25 | 26 | def test_led_disable(GPIO, apa102, FanShim): 27 | fanshim = FanShim(disable_led=True) 28 | 29 | GPIO.setwarnings.assert_called_once_with(False) 30 | GPIO.setmode.assert_called_once_with(GPIO.BCM) 31 | GPIO.setup.assert_has_calls([ 32 | mock.call(fanshim._pin_fancontrol, GPIO.OUT), 33 | mock.call(fanshim._pin_button, GPIO.IN, pull_up_down=GPIO.PUD_UP) 34 | ]) 35 | 36 | assert not apa102.APA102.called 37 | 38 | fanshim.set_light(0, 0, 0) 39 | -------------------------------------------------------------------------------- /library/tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Test configuration. 2 | These allow the mocking of various Python modules 3 | that might otherwise have runtime side-effects. 4 | """ 5 | import sys 6 | import mock 7 | import pytest 8 | 9 | 10 | @pytest.fixture(scope='function', autouse=False) 11 | def FanShim(): 12 | import fanshim 13 | yield fanshim.FanShim 14 | del sys.modules['fanshim'] 15 | 16 | 17 | @pytest.fixture(scope='function', autouse=False) 18 | def GPIO(): 19 | """Mock RPi.GPIO module.""" 20 | GPIO = mock.MagicMock() 21 | # Fudge for Python < 37 (possibly earlier) 22 | sys.modules['RPi'] = mock.Mock() 23 | sys.modules['RPi'].GPIO = GPIO 24 | sys.modules['RPi.GPIO'] = GPIO 25 | yield GPIO 26 | del sys.modules['RPi'] 27 | del sys.modules['RPi.GPIO'] 28 | 29 | 30 | @pytest.fixture(scope='function', autouse=False) 31 | def apa102(): 32 | """Mock APA102 module.""" 33 | apa102 = mock.MagicMock() 34 | sys.modules['apa102'] = apa102 35 | yield apa102 36 | del sys.modules['apa102'] 37 | 38 | 39 | @pytest.fixture(scope='function', autouse=False) 40 | def spidev(): 41 | """Mock spidev module.""" 42 | spidev = mock.MagicMock() 43 | sys.modules['spidev'] = spidev 44 | yield spidev 45 | del sys.modules['spidev'] 46 | 47 | 48 | @pytest.fixture(scope='function', autouse=False) 49 | def atexit(): 50 | """Mock atexit module.""" 51 | atexit = mock.MagicMock() 52 | sys.modules['atexit'] = atexit 53 | yield atexit 54 | del sys.modules['atexit'] 55 | 56 | -------------------------------------------------------------------------------- /examples/manual.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import os 4 | from fanshim import FanShim 5 | 6 | FIFO_NAME = "/tmp/fanshim" 7 | 8 | fanshim = FanShim() 9 | fanshim.set_hold_time(1.0) 10 | fanshim.set_fan(False) 11 | 12 | try: 13 | os.unlink(FIFO_NAME) 14 | except (IOError, OSError): 15 | pass 16 | 17 | os.mkfifo(FIFO_NAME) 18 | 19 | 20 | def handle_command(data): 21 | if data == "on": 22 | fanshim.set_fan(True) 23 | print("Fan SHIM: Fan enabled") 24 | elif data == "off": 25 | fanshim.set_fan(False) 26 | print("Fan SHIM: Fan disabled") 27 | elif len(data) == 6: 28 | r, g, b = data[0:2], data[2:4], data[4:6] 29 | try: 30 | r, g, b = [int(c, 16) for c in (r, g, b)] 31 | except ValueError: 32 | print("Fan SHIM: Invalid colour {c}".format(c=data)) 33 | return 34 | print("Fan SHIM: Setting LED to RGB: {r}, {g}, {b}".format(r=r, g=g, b=b)) 35 | fanshim.set_light(r, g, b) 36 | 37 | 38 | print("""manual.py - Fan SHIM manual control 39 | 40 | Example commands: 41 | 42 | echo "on" > /tmp/fanshim - Turn fan on 43 | echo "off" > /tmp/fanshim - Turn fan off 44 | echo "FF0000" > /tmp/fanshim - Make LED red 45 | echo "00FF00" > /tmp/fanshim - Make LED green 46 | 47 | """) 48 | 49 | while True: 50 | with open(FIFO_NAME, "r") as fifo: 51 | while True: 52 | data = fifo.read().strip() 53 | if len(data) == 0: 54 | break 55 | handle_command(data) 56 | -------------------------------------------------------------------------------- /library/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = fanshim 3 | version = 0.0.5 4 | author = Philip Howard 5 | author_email = phil@pimoroni.com 6 | description = Python library for the Pimoroni Fan Shim for Raspberry Pi 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | keywords = Raspberry Pi 10 | url = https://www.pimoroni.com 11 | project_urls = 12 | GitHub=https://www.github.com/pimoroni/fanshim-python 13 | license = MIT 14 | # This includes the license file(s) in the wheel. 15 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 16 | license_files = LICENSE.txt 17 | classifiers = 18 | Development Status :: 4 - Beta 19 | Operating System :: POSIX :: Linux 20 | License :: OSI Approved :: MIT License 21 | Intended Audience :: Developers 22 | Programming Language :: Python :: 2.6 23 | Programming Language :: Python :: 2.7 24 | Programming Language :: Python :: 3 25 | Topic :: Software Development 26 | Topic :: Software Development :: Libraries 27 | Topic :: System :: Hardware 28 | 29 | [options] 30 | python_requires = >= 2.7 31 | packages = fanshim 32 | install_requires = 33 | apa102 >= 0.0.3 34 | 35 | [flake8] 36 | exclude = 37 | .tox, 38 | .eggs, 39 | .git, 40 | __pycache__, 41 | build, 42 | dist 43 | ignore = 44 | E501 45 | 46 | [pimoroni] 47 | py3deps = 48 | python3-rpi.gpio 49 | python3-setuptools 50 | python3-dev 51 | python3-psutil 52 | py2deps = 53 | python-rpi.gpio 54 | python-setuptools 55 | python-dev 56 | python-psutil 57 | commands = 58 | printf "Enjoy your Fan SHIM!\n" 59 | configtxt = 60 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # led.py 2 | 3 | Basic demonstration of Fan Shim's RGB LED. Cycles around the circumference of the Hue/Saturation/Value colourspace. 4 | 5 | # button.py 6 | 7 | Demonstrates usage of button press, release and hold handlers. 8 | 9 | # toggle.py 10 | 11 | Demonstrates toggling the fan on and off with the button. 12 | 13 | # manual.py 14 | 15 | A barebones demonstration of how to control Fan SHIM manually. 16 | 17 | Since a Python script writing a GPIO pin and exiting can have unpredictable effects, this example shows how you might craft a "service" that runs continuously and ensures the Fan's GPIO pin is asserted either on/off, and that the LED is continuously driven. 18 | 19 | # automatic.py 20 | 21 | Complete example for monitoring temperature and automatic fan control. 22 | 23 | * A long press on the button will toggle automatic mode off/on 24 | * A short press - when automatic is off - will toggle the fan 25 | 26 | The LED will fade between green (cool) to red (hot) as the Pi's temperature changes. 27 | 28 | The script supports these arguments: 29 | 30 | * `--on-threshold N` the temperature at which to turn the fan on, in degrees C (default 65) 31 | * `--off-threshold N` the temperature at which to turn the fan off, in degrees C (default 55) 32 | * `--delay N` the delay between subsequent temperature readings, in seconds (default 2) 33 | * `--preempt` preemptively kick in the fan when the CPU frequency is raised (default off) 34 | * `--brightness` the brightness (value of HSV) of the LED (0-255, default 255) 35 | 36 | Deprecated arguments 37 | 38 | * `--threshold N` the temperature at which the fan should turn on, in degrees C (default 55) 39 | * `--hysteresis N` the change in temperature needed to trigger a fan state change, in degrees C (default 5) 40 | 41 | You can use systemd or crontab to run this example as a fan controller service on your Pi. 42 | 43 | To use systemd, just run: 44 | 45 | ``` 46 | sudo ./install-service.sh 47 | ``` 48 | 49 | You can then stop the fan service with: 50 | 51 | ``` 52 | sudo systemctl stop pimoroni-fanshim.service 53 | ``` 54 | 55 | If you need to change the threshold, hysteresis or delay you can add them as arguments to the installer: 56 | 57 | ``` 58 | sudo ./install-service.sh --on-threshold 65 --off-threshold 55 --delay 2 59 | ``` 60 | 61 | To enable CPU-frequency based control: 62 | 63 | ``` 64 | sudo ./install-service.sh --on-threshold 65 --off-threshold 55 --delay 2 --preempt 65 | ``` 66 | 67 | You can also add `--noled` to disable LED control and/or `--nobutton` to disable button input. 68 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIBRARY_VERSION=$(shell grep version library/setup.cfg | awk -F" = " '{print $$2}') 2 | LIBRARY_NAME=$(shell grep name library/setup.cfg | awk -F" = " '{print $$2}') 3 | 4 | .PHONY: usage install uninstall 5 | usage: 6 | @echo "Library: ${LIBRARY_NAME}" 7 | @echo "Version: ${LIBRARY_VERSION}\n" 8 | @echo "Usage: make , where target is one of:\n" 9 | @echo "install: install the library locally from source" 10 | @echo "uninstall: uninstall the local library" 11 | @echo "check: peform basic integrity checks on the codebase" 12 | @echo "python-readme: generate library/README.md from README.md + library/CHANGELOG.txt" 13 | @echo "python-wheels: build python .whl files for distribution" 14 | @echo "python-sdist: build python source distribution" 15 | @echo "python-clean: clean python build and dist directories" 16 | @echo "python-dist: build all python distribution files" 17 | @echo "python-testdeploy: build all and deploy to test PyPi" 18 | @echo "tag: tag the repository with the current version" 19 | 20 | install: 21 | ./install.sh 22 | 23 | uninstall: 24 | ./uninstall.sh 25 | 26 | check: 27 | @echo "Checking for trailing whitespace" 28 | @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO 29 | @echo "Checking for DOS line-endings" 30 | @! grep -lIUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile 31 | @echo "Checking library/CHANGELOG.txt" 32 | @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} 33 | @echo "Checking library/${LIBRARY_NAME}/__init__.py" 34 | @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" 35 | 36 | tag: 37 | git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" 38 | 39 | python-readme: library/README.md 40 | 41 | python-license: library/LICENSE.txt 42 | 43 | library/README.md: README.md library/CHANGELOG.txt 44 | cp README.md library/README.md 45 | printf "\n# Changelog\n" >> library/README.md 46 | cat library/CHANGELOG.txt >> library/README.md 47 | 48 | library/LICENSE.txt: LICENSE 49 | cp LICENSE library/LICENSE.txt 50 | 51 | python-wheels: python-readme python-license 52 | cd library; python3 setup.py bdist_wheel 53 | cd library; python setup.py bdist_wheel 54 | 55 | python-sdist: python-readme python-license 56 | cd library; python setup.py sdist 57 | 58 | python-clean: 59 | -rm -r library/dist 60 | -rm -r library/build 61 | -rm -r library/*.egg-info 62 | 63 | python-dist: python-clean python-wheels python-sdist 64 | ls library/dist 65 | 66 | python-testdeploy: python-dist 67 | twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* 68 | 69 | python-deploy: check python-dist 70 | twine upload library/dist/* 71 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CONFIG=/boot/config.txt 4 | DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` 5 | CONFIG_BACKUP=false 6 | CODENAME=`lsb_release -sc` 7 | 8 | if [ $? -ne 0 ]; then 9 | printf "Error parsing configuration...\n" 10 | exit 1 11 | fi 12 | 13 | function do_config_backup { 14 | if [ ! $CONFIG_BACKUP == true ]; then 15 | CONFIG_BACKUP=true 16 | FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" 17 | printf "Backing up $CONFIG to $FILENAME\n" 18 | cp $CONFIG $FILENAME 19 | fi 20 | } 21 | 22 | function apt_pkg_install { 23 | PACKAGES=() 24 | PACKAGES_IN=("$@") 25 | for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do 26 | PACKAGE="${PACKAGES_IN[$i]}" 27 | printf "Checking for $PACKAGE\n" 28 | dpkg -L $PACKAGE > /dev/null 2>&1 29 | if [ "$?" == "1" ]; then 30 | PACKAGES+=("$PACKAGE") 31 | fi 32 | done 33 | PACKAGES="${PACKAGES[@]}" 34 | if ! [ "$PACKAGES" == "" ]; then 35 | echo "Installing missing packages: $PACKAGES" 36 | sudo apt update 37 | sudo apt install -y $PACKAGES 38 | fi 39 | } 40 | 41 | if [[ $CODENAME != "bullseye" ]]; then 42 | apt_pkg_install python-configparser 43 | fi 44 | 45 | CONFIG_VARS=`python - <=0.7.0 (for Pi 4 support)\n" 84 | python - <=0.7.0" 95 | else 96 | printf "rpi.gpio >= 0.7.0 already installed\n" 97 | fi 98 | 99 | apt_pkg_install "${PY2_DEPS[@]}" 100 | python setup.py install 101 | fi 102 | 103 | if [ -f "/usr/bin/python3" ]; then 104 | printf "Installing for Python 3..\n" 105 | apt_pkg_install "${PY3_DEPS[@]}" 106 | python3 setup.py install 107 | fi 108 | 109 | cd .. 110 | 111 | for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do 112 | CMD="${SETUP_CMDS[$i]}" 113 | # Attempt to catch anything that touches /boot/config.txt and trigger a backup 114 | if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then 115 | do_config_backup 116 | fi 117 | eval $CMD 118 | done 119 | 120 | for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do 121 | CONFIG_LINE="${CONFIG_TXT[$i]}" 122 | if ! [ "$CONFIG_LINE" == "" ]; then 123 | do_config_backup 124 | sed -i 's/^#$CONFIG_LINE/$CONFIG_LINE/' $CONFIG 125 | if ! grep -q -E "^$CONFIG_LINE" $CONFIG; then 126 | printf "$CONFIG_LINE\n" >> $CONFIG 127 | fi 128 | fi 129 | done 130 | 131 | printf "Done!\n" 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fan Shim for Raspberry Pi 2 | 3 | [![Build Status](https://travis-ci.com/pimoroni/fanshim-python.svg?branch=master)](https://travis-ci.com/pimoroni/fanshim-python) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/fanshim-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/fanshim-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/fanshim.svg)](https://pypi.python.org/pypi/fanshim) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/fanshim.svg)](https://pypi.python.org/pypi/fanshim) 7 | 8 | # Installing 9 | 10 | Stable library from PyPi: 11 | 12 | * Just run `sudo pip install fanshim` 13 | 14 | Latest/development library from GitHub: 15 | 16 | * `apt install git python3-pip` 17 | * `git clone https://github.com/pimoroni/fanshim-python` 18 | * `cd fanshim-python` 19 | * `sudo ./install.sh` 20 | 21 | # Reference 22 | 23 | You should first set up an instance of the `FANShim` class, eg: 24 | 25 | ```python 26 | from fanshim import FanShim 27 | fanshim = FanShim() 28 | ``` 29 | 30 | ## Fan 31 | 32 | Turn the fan on with: 33 | 34 | ```python 35 | fanshim.set_fan(True) 36 | ``` 37 | 38 | Turn it off with: 39 | 40 | ```python 41 | fanshim.set_fan(False) 42 | ``` 43 | 44 | You can also toggle the fan with: 45 | 46 | ```python 47 | fanshim.toggle_fan() 48 | ``` 49 | 50 | You can check the status of the fan with: 51 | 52 | ```python 53 | fanshim.get_fan() # returns 1 for 'on', 0 for 'off' 54 | ``` 55 | 56 | ## LED 57 | 58 | Fan Shim includes one RGB APA-102 LED. 59 | 60 | Set it to any colour with: 61 | 62 | ```python 63 | fanshim.set_light(r, g, b) 64 | ``` 65 | 66 | Arguments r, g and b should be numbers between 0 and 255 that describe the colour you want. 67 | 68 | For example, full red: 69 | 70 | ``` 71 | fanshim.set_light(255, 0, 0) 72 | ``` 73 | 74 | ## Button 75 | 76 | Fan Shim includes a button, you can bind actions to press, release and hold events. 77 | 78 | Do something when the button is pressed: 79 | 80 | ```python 81 | @fanshim.on_press() 82 | def button_pressed(): 83 | print("The button has been pressed!") 84 | ``` 85 | 86 | Or when it has been released: 87 | 88 | ```python 89 | @fanshim.on_release() 90 | def button_released(was_held): 91 | print("The button has been pressed!") 92 | ``` 93 | 94 | Or when it's been pressed long enough to trigger a hold: 95 | 96 | ```python 97 | fanshim.set_hold_time(2.0) 98 | 99 | @fanshim.on_hold() 100 | def button_held(): 101 | print("The button was held for 2 seconds") 102 | ``` 103 | 104 | The function you bind to `on_release()` is passed a `was_held` parameter, 105 | this lets you know if the button was held down for longer than the configured 106 | hold time. If you want to bind an action to "press" and another to "hold" you 107 | should check this flag and perform your action in the `on_release()` handler: 108 | 109 | ```python 110 | @fanshim.on_release() 111 | def button_released(was_held): 112 | if was_held: 113 | print("Long press!") 114 | else: 115 | print("Short press!") 116 | ``` 117 | 118 | To configure the amount of time the button should be held (in seconds), use: 119 | 120 | ```python 121 | fanshim.set_hold_time(number_of_seconds) 122 | ``` 123 | 124 | If you need to stop Fan Shim from polling the button, use: 125 | 126 | ```python 127 | fanshim.stop_polling() 128 | ``` 129 | 130 | You can start it again with: 131 | 132 | ```python 133 | fanshim.start_polling() 134 | ``` 135 | 136 | # Alternate Software 137 | 138 | * Fan SHIM in C, using WiringPi - https://github.com/flobernd/raspi-fanshim 139 | * Fan SHIM in C++, using libgpiod - https://github.com/daviehh/fanshim-cpp 140 | 141 | -------------------------------------------------------------------------------- /library/README.md: -------------------------------------------------------------------------------- 1 | # Fan Shim for Raspberry Pi 2 | 3 | [![Build Status](https://travis-ci.com/pimoroni/fanshim-python.svg?branch=master)](https://travis-ci.com/pimoroni/fanshim-python) 4 | [![Coverage Status](https://coveralls.io/repos/github/pimoroni/fanshim-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/fanshim-python?branch=master) 5 | [![PyPi Package](https://img.shields.io/pypi/v/fanshim.svg)](https://pypi.python.org/pypi/fanshim) 6 | [![Python Versions](https://img.shields.io/pypi/pyversions/fanshim.svg)](https://pypi.python.org/pypi/fanshim) 7 | 8 | # Installing 9 | 10 | Stable library from PyPi: 11 | 12 | * Just run `sudo pip install fanshim` 13 | 14 | Latest/development library from GitHub: 15 | 16 | * `apt install git python3-pip` 17 | * `git clone https://github.com/pimoroni/fanshim-python` 18 | * `cd fanshim-python` 19 | * `sudo ./install.sh` 20 | 21 | # Reference 22 | 23 | You should first set up an instance of the `FANShim` class, eg: 24 | 25 | ```python 26 | from fanshim import FanShim 27 | fanshim = FanShim() 28 | ``` 29 | 30 | ## Fan 31 | 32 | Turn the fan on with: 33 | 34 | ```python 35 | fanshim.set_fan(True) 36 | ``` 37 | 38 | Turn it off with: 39 | 40 | ```python 41 | fanshim.set_fan(False) 42 | ``` 43 | 44 | You can also toggle the fan with: 45 | 46 | ```python 47 | fanshim.toggle_fan() 48 | ``` 49 | 50 | You can check the status of the fan with: 51 | 52 | ```python 53 | fanshim.get_fan() # returns 1 for 'on', 0 for 'off' 54 | ``` 55 | 56 | ## LED 57 | 58 | Fan Shim includes one RGB APA-102 LED. 59 | 60 | Set it to any colour with: 61 | 62 | ```python 63 | fanshim.set_light(r, g, b) 64 | ``` 65 | 66 | Arguments r, g and b should be numbers between 0 and 255 that describe the colour you want. 67 | 68 | For example, full red: 69 | 70 | ``` 71 | fanshim.set_light(255, 0, 0) 72 | ``` 73 | 74 | ## Button 75 | 76 | Fan Shim includes a button, you can bind actions to press, release and hold events. 77 | 78 | Do something when the button is pressed: 79 | 80 | ```python 81 | @fanshim.on_press() 82 | def button_pressed(): 83 | print("The button has been pressed!") 84 | ``` 85 | 86 | Or when it has been released: 87 | 88 | ```python 89 | @fanshim.on_release() 90 | def button_released(was_held): 91 | print("The button has been pressed!") 92 | ``` 93 | 94 | Or when it's been pressed long enough to trigger a hold: 95 | 96 | ```python 97 | fanshim.set_hold_time(2.0) 98 | 99 | @fanshim.on_hold() 100 | def button_held(): 101 | print("The button was held for 2 seconds") 102 | ``` 103 | 104 | The function you bind to `on_release()` is passed a `was_held` parameter, 105 | this lets you know if the button was held down for longer than the configured 106 | hold time. If you want to bind an action to "press" and another to "hold" you 107 | should check this flag and perform your action in the `on_release()` handler: 108 | 109 | ```python 110 | @fanshim.on_release() 111 | def button_released(was_held): 112 | if was_held: 113 | print("Long press!") 114 | else: 115 | print("Short press!") 116 | ``` 117 | 118 | To configure the amount of time the button should be held (in seconds), use: 119 | 120 | ```python 121 | fanshim.set_hold_time(number_of_seconds) 122 | ``` 123 | 124 | If you need to stop Fan Shim from polling the button, use: 125 | 126 | ```python 127 | fanshim.stop_polling() 128 | ``` 129 | 130 | You can start it again with: 131 | 132 | ```python 133 | fanshim.start_polling() 134 | ``` 135 | 136 | # Alternate Software 137 | 138 | * Fan SHIM in C, using WiringPi - https://github.com/flobernd/raspi-fanshim 139 | * Fan SHIM in C++, using libgpiod - https://github.com/daviehh/fanshim-cpp 140 | 141 | 142 | # Changelog 143 | 0.0.5 144 | ----- 145 | 146 | * Replace Plasma API with APA102 library 147 | * Add support for setting LED global brightness 148 | * Add support for disabling button and/or LED 149 | * Move packages/requires to setup.config, minimum version now Python 2.7 150 | 151 | 0.0.4 152 | ----- 153 | 154 | * Prepare Fan SHIM to use legacy Plasma API 155 | 156 | 0.0.3 157 | ----- 158 | 159 | * Fix: lower polling frequency and make customisable, for PR #6 160 | 161 | 0.0.2 162 | ----- 163 | 164 | * Fix: Fix error on exit 165 | 166 | 0.0.1 167 | ----- 168 | 169 | * Initial Release 170 | -------------------------------------------------------------------------------- /library/fanshim/__init__.py: -------------------------------------------------------------------------------- 1 | import RPi.GPIO as GPIO 2 | import time 3 | import apa102 4 | import atexit 5 | from threading import Thread 6 | 7 | __version__ = '0.0.5' 8 | 9 | 10 | class FanShim(): 11 | def __init__(self, pin_fancontrol=18, pin_button=17, button_poll_delay=0.05, disable_button=False, disable_led=False): 12 | """FAN Shim. 13 | 14 | :param pin_fancontrol: BCM pin for fan on/off 15 | :param pin_button: BCM pin for button 16 | 17 | """ 18 | self._pin_fancontrol = pin_fancontrol 19 | self._pin_button = pin_button 20 | self._poll_delay = button_poll_delay 21 | self._button_press_handler = None 22 | self._button_release_handler = None 23 | self._button_hold_handler = None 24 | self._button_hold_time = 2.0 25 | self._t_poll = None 26 | 27 | self._disable_button = disable_button 28 | self._disable_led = disable_led 29 | 30 | GPIO.setwarnings(False) 31 | GPIO.setmode(GPIO.BCM) 32 | GPIO.setup(self._pin_fancontrol, GPIO.OUT) 33 | 34 | if not self._disable_button: 35 | GPIO.setup(self._pin_button, GPIO.IN, pull_up_down=GPIO.PUD_UP) 36 | 37 | if not self._disable_led: 38 | self._led = apa102.APA102(1, 15, 14, None, brightness=0.05) 39 | 40 | atexit.register(self._cleanup) 41 | 42 | def start_polling(self): 43 | """Start button polling.""" 44 | if self._disable_button: 45 | return 46 | 47 | if self._t_poll is None: 48 | self._t_poll = Thread(target=self._run) 49 | self._t_poll.daemon = True 50 | self._t_poll.start() 51 | 52 | def stop_polling(self): 53 | """Stop button polling.""" 54 | if self._disable_button: 55 | return 56 | 57 | if self._t_poll is not None: 58 | self._running = False 59 | self._t_poll.join() 60 | 61 | def on_press(self, handler=None): 62 | """Attach function to button press event.""" 63 | def attach_handler(handler): 64 | self._button_press_handler = handler 65 | self.start_polling() 66 | 67 | if handler is not None: 68 | attach_handler(handler) 69 | else: 70 | return attach_handler 71 | 72 | def on_release(self, handler=None): 73 | """Attach function to button release event.""" 74 | def attach_handler(handler): 75 | self._button_release_handler = handler 76 | self.start_polling() 77 | 78 | if handler is not None: 79 | attach_handler(handler) 80 | else: 81 | return attach_handler 82 | 83 | def on_hold(self, handler=None): 84 | """Attach function to button hold event.""" 85 | def attach_handler(handler): 86 | self._button_hold_handler = handler 87 | self.start_polling() 88 | 89 | if handler is not None: 90 | attach_handler(handler) 91 | else: 92 | return attach_handler 93 | 94 | def set_hold_time(self, hold_time): 95 | """Set the button hold time in seconds. 96 | 97 | :param hold_time: Amount of time button must be held to trigger on_hold (in seconds) 98 | 99 | """ 100 | self._button_hold_time = hold_time 101 | 102 | def get_fan(self): 103 | """Get current fan state.""" 104 | return GPIO.input(self._pin_fancontrol) 105 | 106 | def toggle_fan(self): 107 | """Toggle fan state.""" 108 | return self.set_fan(False if self.get_fan() else True) 109 | 110 | def set_fan(self, fan_state): 111 | """Set the fan on/off. 112 | 113 | :param fan_state: True/False for on/off 114 | 115 | """ 116 | GPIO.output(self._pin_fancontrol, True if fan_state else False) 117 | return True if fan_state else False 118 | 119 | def set_light(self, r, g, b, brightness=None): 120 | """Set LED. 121 | 122 | :param r: Red (0-255) 123 | :param g: Green (0-255) 124 | :param b: Blue (0-255) 125 | 126 | """ 127 | if self._disable_led: 128 | return 129 | 130 | self._led.set_pixel(0, r, g, b) 131 | if brightness is not None: 132 | self._led.set_brightness(0, brightness) 133 | self._led.show() 134 | 135 | def _cleanup(self): 136 | self.stop_polling() 137 | 138 | self.set_light(0, 0, 0) 139 | 140 | def _run(self): 141 | self._running = True 142 | last = 1 143 | 144 | while self._running: 145 | current = GPIO.input(self._pin_button) 146 | # Transition from 1 to 0 147 | if last > current: 148 | self._t_pressed = time.time() 149 | self._hold_fired = False 150 | 151 | if callable(self._button_press_handler): 152 | self._t_repeat = time.time() 153 | Thread(target=self._button_press_handler).start() 154 | 155 | if last < current: 156 | if callable(self._button_release_handler): 157 | Thread(target=self._button_release_handler, args=(self._hold_fired,)).start() 158 | 159 | if current == 0: 160 | if not self._hold_fired and (time.time() - self._t_pressed) > self._button_hold_time: 161 | if callable(self._button_hold_handler): 162 | Thread(target=self._button_hold_handler).start() 163 | self._hold_fired = True 164 | 165 | last = current 166 | 167 | time.sleep(self._poll_delay) 168 | -------------------------------------------------------------------------------- /examples/automatic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from fanshim import FanShim 3 | from threading import Lock 4 | import colorsys 5 | import psutil 6 | import argparse 7 | import time 8 | import signal 9 | import sys 10 | 11 | 12 | parser = argparse.ArgumentParser() 13 | parser.add_argument('--off-threshold', type=float, default=55.0, help='Temperature threshold in degrees C to enable fan') 14 | parser.add_argument('--on-threshold', type=float, default=65.0, help='Temperature threshold in degrees C to disable fan') 15 | parser.add_argument('--low-temp', type=float, default=None, help='Temperature at which the LED is greeen') 16 | parser.add_argument('--high-temp', type=float, default=None, help='Temperature for which LED is red') 17 | parser.add_argument('--delay', type=float, default=2.0, help='Delay, in seconds, between temperature readings') 18 | parser.add_argument('--preempt', action='store_true', default=False, help='Monitor CPU frequency and activate cooling premptively') 19 | parser.add_argument('--verbose', action='store_true', default=False, help='Output temp and fan status messages') 20 | parser.add_argument('--nobutton', action='store_true', default=False, help='Disable button input') 21 | parser.add_argument('--noled', action='store_true', default=False, help='Disable LED control') 22 | parser.add_argument('--brightness', type=float, default=255.0, help='LED brightness, from 0 to 255') 23 | parser.add_argument('--extended-colours', action='store_true', default=False, help='Extend LED colours for outside of normal low to high range') 24 | 25 | args = parser.parse_args() 26 | 27 | 28 | def clean_exit(signum, frame): 29 | set_fan(False) 30 | if not args.noled: 31 | fanshim.set_light(0, 0, 0) 32 | sys.exit(0) 33 | 34 | 35 | def update_led_temperature(temp): 36 | led_busy.acquire() 37 | temp = float(temp) 38 | if temp < args.low_temp and args.extended_colours: 39 | # Between minimum temp and low temp, set LED to blue through to green 40 | temp -= min_temp 41 | temp /= float(args.low_temp - min_temp) 42 | temp = max(0, temp) 43 | hue = (120.0 / 360.0) + ((1.0 - temp) * 120.0 / 360.0) 44 | elif temp > args.high_temp and args.extended_colours: 45 | # Between high temp and maximum temp, set LED to red through to magenta 46 | temp -= args.high_temp 47 | temp /= float(max_temp - args.high_temp) 48 | temp = min(1, temp) 49 | hue = 1.0 - (temp * 60.0 / 360.0) 50 | else: 51 | # In the normal low temp to high temp range, set LED to green through to red 52 | temp -= args.low_temp 53 | temp /= float(args.high_temp - args.low_temp) 54 | temp = max(0, min(1, temp)) 55 | hue = (1.0 - temp) * 120.0 / 360.0 56 | 57 | r, g, b = [int(c * 255.0) for c in colorsys.hsv_to_rgb(hue, 1.0, args.brightness / 255.0)] 58 | fanshim.set_light(r, g, b) 59 | led_busy.release() 60 | 61 | 62 | def get_cpu_temp(): 63 | t = psutil.sensors_temperatures() 64 | for x in ['cpu-thermal', 'cpu_thermal']: 65 | if x in t: 66 | return t[x][0].current 67 | print("Warning: Unable to get CPU temperature!") 68 | return 0 69 | 70 | 71 | def get_cpu_freq(): 72 | freq = psutil.cpu_freq() 73 | return freq 74 | 75 | 76 | def set_fan(status): 77 | global enabled 78 | changed = False 79 | if status != enabled: 80 | changed = True 81 | fanshim.set_fan(status) 82 | enabled = status 83 | return changed 84 | 85 | 86 | def set_automatic(status): 87 | global armed, last_change 88 | armed = status 89 | last_change = 0 90 | 91 | 92 | fanshim = FanShim(disable_button=args.nobutton, disable_led=args.noled) 93 | fanshim.set_hold_time(1.0) 94 | fanshim.set_fan(False) 95 | armed = True 96 | enabled = False 97 | led_busy = Lock() 98 | enable = False 99 | is_fast = False 100 | last_change = 0 101 | min_temp = 30 102 | max_temp = 85 103 | signal.signal(signal.SIGTERM, clean_exit) 104 | 105 | if args.noled: 106 | led_busy.acquire() 107 | fanshim.set_light(0, 0, 0) 108 | led_busy.release() 109 | 110 | if args.low_temp is None: 111 | args.low_temp = args.off_threshold 112 | 113 | if args.high_temp is None: 114 | args.high_temp = args.on_threshold 115 | 116 | if not args.nobutton: 117 | @fanshim.on_release() 118 | def release_handler(was_held): 119 | global armed 120 | if was_held: 121 | set_automatic(not armed) 122 | elif not armed: 123 | set_fan(not enabled) 124 | 125 | @fanshim.on_hold() 126 | def held_handler(): 127 | global led_busy 128 | if args.noled: 129 | return 130 | led_busy.acquire() 131 | for _ in range(3): 132 | fanshim.set_light(0, 0, 255) 133 | time.sleep(0.04) 134 | fanshim.set_light(0, 0, 0) 135 | time.sleep(0.04) 136 | led_busy.release() 137 | 138 | 139 | try: 140 | while True: 141 | t = get_cpu_temp() 142 | f = get_cpu_freq() 143 | was_fast = is_fast 144 | is_fast = (int(f.current) == int(f.max)) 145 | if args.verbose: 146 | print("Current: {:05.02f} Target: {:05.02f} Freq {: 5.02f} Automatic: {} On: {}".format(t, args.off_threshold, f.current / 1000.0, armed, enabled)) 147 | 148 | if args.preempt and is_fast and was_fast: 149 | enable = True 150 | elif armed: 151 | if t >= args.on_threshold: 152 | enable = True 153 | elif t <= args.off_threshold: 154 | enable = False 155 | 156 | if set_fan(enable): 157 | last_change = t 158 | 159 | if not args.noled: 160 | update_led_temperature(t) 161 | 162 | time.sleep(args.delay) 163 | except KeyboardInterrupt: 164 | pass 165 | -------------------------------------------------------------------------------- /examples/install-service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 4 | ON_THRESHOLD=65 5 | OFF_THRESHOLD=55 6 | HYSTERESIS=5 7 | DELAY=2 8 | PREEMPT="no" 9 | POSITIONAL_ARGS=() 10 | NOLED="no" 11 | NOBUTTON="no" 12 | BRIGHTNESS=255 13 | EXTCOLOURS="no" 14 | PYTHON="python3" 15 | PIP="pip3" 16 | PSUTIL_MIN_VERSION="5.6.7" 17 | 18 | ON_THRESHOLD_SET=false 19 | OFF_THRESHOLD_SET=false 20 | 21 | SERVICE_PATH=/etc/systemd/system/pimoroni-fanshim.service 22 | 23 | USAGE="sudo ./install-service.sh --off-threshold --on-threshold --delay --brightness --low-temp --high-temp --venv (--preempt) (--noled) (--nobutton) (--extended-colours)" 24 | 25 | # Convert Python path to absolute for systemd 26 | PYTHON=`type -P $PYTHON` 27 | 28 | while [[ $# -gt 0 ]]; do 29 | K="$1" 30 | case $K in 31 | -p|--preempt) 32 | if [ "$2" == "yes" ] || [ "$2" == "no" ]; then 33 | PREEMPT="$2" 34 | shift 35 | else 36 | PREEMPT="yes" 37 | fi 38 | shift 39 | ;; 40 | -l|--noled) 41 | if [ "$2" == "yes" ] || [ "$2" == "no" ]; then 42 | NOLED="$2" 43 | shift 44 | else 45 | NOLED="yes" 46 | fi 47 | shift 48 | ;; 49 | -b|--nobutton) 50 | if [ "$2" == "yes" ] || [ "$2" == "no" ]; then 51 | NOBUTTON="$2" 52 | shift 53 | else 54 | NOBUTTON="yes" 55 | fi 56 | shift 57 | ;; 58 | -o|--on-threshold) 59 | ON_THRESHOLD="$2" 60 | ON_THRESHOLD_SET=true 61 | shift 62 | shift 63 | ;; 64 | -f|--off-threshold) 65 | OFF_THRESHOLD="$2" 66 | OFF_THRESHOLD_SET=true 67 | shift 68 | shift 69 | ;; 70 | -G|--low-temp) 71 | LOW_TEMP="$2" 72 | shift 73 | shift 74 | ;; 75 | -R|--high-temp) 76 | HIGH_TEMP="$2" 77 | shift 78 | shift 79 | ;; 80 | -d|--delay) 81 | DELAY="$2" 82 | shift 83 | shift 84 | ;; 85 | -r|--brightness) 86 | BRIGHTNESS="$2" 87 | shift 88 | shift 89 | ;; 90 | --venv) 91 | VENV="$(realpath ${2%/})/bin" 92 | PYTHON="$VENV/python3" 93 | PIP="$VENV/pip3" 94 | shift 95 | shift 96 | ;; 97 | -x|--extended-colours) 98 | if [ "$2" == "yes" ] || [ "$2" == "no" ]; then 99 | EXTCOLOURS="$2" 100 | shift 101 | else 102 | EXTCOLOURS="yes" 103 | fi 104 | shift 105 | ;; 106 | *) 107 | if [[ $1 == -* ]]; then 108 | printf "Unrecognised option: $1\n"; 109 | printf "Usage: $USAGE\n"; 110 | exit 1 111 | fi 112 | POSITIONAL_ARGS+=("$1") 113 | shift 114 | esac 115 | done 116 | 117 | if ! ( type -P "$PYTHON" > /dev/null ) ; then 118 | if [ "$PYTHON" == "python3" ]; then 119 | printf "Fan SHIM controller requires Python 3\n" 120 | printf "You should run: 'sudo apt install python3'\n" 121 | else 122 | printf "Cannot find virtual environment.\n" 123 | printf "Set to base of virtual environment i.e. /bin/python3.\n" 124 | fi 125 | exit 1 126 | fi 127 | 128 | if ! ( type -P "$PIP" > /dev/null ) ; then 129 | printf "Fan SHIM controller requires Python 3 pip\n" 130 | if [ "$PIP" == "pip3" ]; then 131 | printf "You should run: 'sudo apt install python3-pip'\n" 132 | else 133 | printf "Ensure that your virtual environment has pip3 installed.\n" 134 | fi 135 | exit 1 136 | fi 137 | 138 | set -- "${POSITIONAL_ARGS[@]}" 139 | 140 | EXTRA_ARGS="" 141 | 142 | if [ "$PREEMPT" == "yes" ]; then 143 | EXTRA_ARGS+=' --preempt' 144 | fi 145 | 146 | if [ "$NOLED" == "yes" ]; then 147 | EXTRA_ARGS+=' --noled' 148 | fi 149 | 150 | if [ "$NOBUTTON" == "yes" ]; then 151 | EXTRA_ARGS+=' --nobutton' 152 | fi 153 | 154 | if [ "$EXTCOLOURS" == "yes" ]; then 155 | EXTRA_ARGS+=' --extended-colours' 156 | fi 157 | 158 | if ! [ "$1" == "" ]; then 159 | if [ $ON_THRESHOLD_SET ]; then 160 | printf "Refusing to overwrite explicitly set On Threshold ($ON_THRESHOLD) with positional argument!\n" 161 | printf "Please double-check your arguments and use one or the other!\n" 162 | exit 1 163 | fi 164 | ON_THRESHOLD=$1 165 | fi 166 | 167 | if ! [ "$2" == "" ]; then 168 | if [ $OFF_THRESHOLD_SET ]; then 169 | printf "Refusing to overwrite explicitly set Off Threshold ($OFF_THRESHOLD) with positional argument!\n" 170 | printf "Please double-check your arguments and use one or the other!\n" 171 | exit 1 172 | fi 173 | (( OFF_THRESHOLD = ON_THRESHOLD - $2 )) 174 | fi 175 | 176 | if [ "$LOW_TEMP" == "" ]; then 177 | LOW_TEMP=$OFF_THRESHOLD 178 | fi 179 | 180 | if [ "$HIGH_TEMP" == "" ]; then 181 | HIGH_TEMP=$ON_THRESHOLD 182 | fi 183 | 184 | cat << EOF 185 | Setting up with: 186 | Off Threshold: $OFF_THRESHOLD C 187 | On Threshold: $ON_THRESHOLD C 188 | Low Temp: $LOW_TEMP C 189 | High Temp: $HIGH_TEMP C 190 | Delay: $DELAY seconds 191 | Preempt: $PREEMPT 192 | Disable LED: $NOLED 193 | Disable Button: $NOBUTTON 194 | Brightness: $BRIGHTNESS 195 | Extended Colours: $EXTCOLOURS 196 | 197 | To change these options, run: 198 | $USAGE 199 | 200 | Or edit: $SERVICE_PATH 201 | 202 | 203 | EOF 204 | 205 | read -r -d '' UNIT_FILE << EOF 206 | [Unit] 207 | Description=Fan Shim Service 208 | After=multi-user.target 209 | 210 | [Service] 211 | Type=simple 212 | WorkingDirectory=$(pwd) 213 | ExecStart=$PYTHON $(pwd)/automatic.py --on-threshold $ON_THRESHOLD --off-threshold $OFF_THRESHOLD --low-temp $LOW_TEMP --high-temp $HIGH_TEMP --delay $DELAY --brightness $BRIGHTNESS $EXTRA_ARGS 214 | Restart=on-failure 215 | 216 | [Install] 217 | WantedBy=multi-user.target 218 | EOF 219 | 220 | printf "Checking for rpi.gpio >= 0.7.0 (for Pi 4 support)\n" 221 | $PYTHON - <=0.7.0" 232 | else 233 | printf "rpi.gpio >= 0.7.0 already installed\n" 234 | fi 235 | 236 | printf "Checking for Fan SHIM\n" 237 | $PYTHON - > /dev/null 2>&1 <= $PSUTIL_MIN_VERSION\n" 249 | $PYTHON - > /dev/null 2>&1 <= parse_version('$PSUTIL_MIN_VERSION')) 254 | EOF 255 | 256 | if [ $? -ne 0 ]; then 257 | printf "Installing psutil\n" 258 | $PIP install --ignore-installed psutil 259 | else 260 | printf "psutil >= $PSUTIL_MIN_VERSION already installed\n" 261 | fi 262 | 263 | printf "\nInstalling service to: $SERVICE_PATH\n" 264 | echo "$UNIT_FILE" > $SERVICE_PATH 265 | systemctl daemon-reload 266 | systemctl enable --no-pager pimoroni-fanshim.service 267 | systemctl restart --no-pager pimoroni-fanshim.service 268 | systemctl status --no-pager pimoroni-fanshim.service 269 | --------------------------------------------------------------------------------