pax_global_header00006660000000000000000000000064140136756170014524gustar00rootroot0000000000000052 comment=753325b13990a755f7a2b326ab898aba1f9143ba liquidctl-1.5.1/000077500000000000000000000000001401367561700135225ustar00rootroot00000000000000liquidctl-1.5.1/.github/000077500000000000000000000000001401367561700150625ustar00rootroot00000000000000liquidctl-1.5.1/.github/workflows/000077500000000000000000000000001401367561700171175ustar00rootroot00000000000000liquidctl-1.5.1/.github/workflows/test-from-sources.yml000066400000000000000000000031771401367561700232530ustar00rootroot00000000000000name: tests on: [push, pull_request] jobs: build: runs-on: ${{ matrix.os }} strategy: max-parallel: 8 matrix: python-version: [3.9, 3.8, 3.7, 3.6] os: [ubuntu-latest, macos-latest] exclude: - os: macos-latest python-version: 3.6 steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install system dependencies (Linux) if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y libusb-1.0-0-dev libudev-dev - name: Install system dependencies (macOS) if: matrix.os == 'macos-latest' run: | brew install libusb - name: Install Python dependencies run: | python -m pip install --upgrade pip python setup.py egg_info pip install -r liquidctl.egg-info/requires.txt - name: Run unit tests and module doctests run: | pip install pytest XDG_RUNTIME_DIR=.tests_rundir pytest - name: Install and run the resulting executable run: | pip install . liquidctl list --verbose --debug - name: Lint with flake8 run: | pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=120 --statistics liquidctl-1.5.1/.gitignore000066400000000000000000000001261401367561700155110ustar00rootroot00000000000000*.egg-info .krakenduty-poc __pycache__ build dist liquidctl/extraversion.py .coverage liquidctl-1.5.1/CHANGELOG.md000066400000000000000000000443311401367561700153400ustar00rootroot00000000000000# Changelog ## [1.5.1] – 2021-02-19 _Summary for the 1.5.1 release: fixes to error reporting, handling of runtime data, and other bugs._ Changelog since 1.5.0: ### Fixed - Handle corrupted runtime data (#278) - Fix item prefixes in list output when `--match` is passed - Remove caching of temporarily stored data - Append formated exception to "unknown error" messages - Only attempt to disconnect from a device if already connected - Only attempt to set the USB configuration if no other errors have been detected - Return the context manager when overriding `connect()` - Fix construction of fallback search paths for runtime data ## [1.5.0] – 2021-01-27 _Summary for the 1.5.0 release: Corsair Commander Pro and Lighting Node Pro support; EVGA GTX 1080 FTW and ASUS Strix RTX 2080 Ti OC support on Linux; Corsair Vengeance RGB and TSE2004-compatible DDR4 modules support on Intel on Linux; `--direction` flag, replacing previous "backwards-" modes; improved error handling and reporting; new project home; other improvements and fixes._ _Note for Linux package maintainers: this release introduces a new dependency, Python 'smbus' (from the i2c-tools project); additionally, since trying to access I²C/SMBus devices without having the i2c-dev kernel module loaded will result in errors, `extra/linux/modules-load.conf` is provided as a suggestion; finally, `extra/linux/71-liquidctl.rules` will now (as provided) give unprivileged access to i801_smbus adapters._ Changelog since 1.4.2: ### Added - Add SMBus and I²C support on Linux - Add support for EVGA GTX 1080 FTW on Linux - Add support for ASUS Strix RTX 2080 Ti OC on Linux - Add experimental support for DIMMs with TSE2004-compatible temperature sensors on Intel/Linux - Add experimental support for Corsair Vengeance RGB on Intel/Linux - Add experimental support for the Corsair Commander Pro - Add experimental support for the Corsair Lighting Node Pro - Add `--direction` modifier to animations - Add `--non-volatile` to control persistence of settings (NVIDIA GPUs) - Add `--start-led`, `--maximum-leds` and `--temperature-sensor` options (Corsair Commander/Lighting Node devices) - Add support for CSS-style hexadecimal triples - Implement the context manager protocol in the driver API - Export `find_liquidctl_devices` from the top-level `liquidctl` package - Add modules-load configuration file for Linux - Add completion script for bash - [extra] Add `LQiNFO.py` exporter (liquidctl -> HWiNFO) - [extra] Add `prometheus-liquidctl-exporter` exporter (liquidctl -> Prometheus) ### Changed - Move GitHub project into liquidctl organization - Improve error handling and reporting - Make vendor and product IDs optional in drivers - Mark Kraken X53, X63, X73 as no longer experimental - Mark NZXT RGB & Fan Controller as no longer experimental - Mark RGB Fusion 2.0 controllers as no longer experimental - Change casing of "PRO" device names to "Pro" - Improve the documentation ### Fixed - Fix potential exception when a release number is not available - Enforce USB port filters on HID devices - Fix backwards `rainbow-pulse` mode on Kraken X3 devices - Fix compatibility with hidapi 0.10 and multi-usage devices (RGB Fusion 2.0 controllers) - Fix lighting settings in Platinum SE and Pro XT coolers - Generate and verify the checksums of zip and exe built on AppVeyor ### Deprecated - Deprecate `backwards-` pseudo modes; use `--direction=backwards` instead ### Checksums ``` 370eb9c662111b51465ac5e2649f7eaf423bd22799ef983c4957468e9d957c15 liquidctl-1.5.0-bin-windows-x86_64.zip 762561a8b491aa98f0ccbbab4f9770813a82cc7fd776fa4c21873b994d63e892 liquidctl-1.5.0.tar.gz ``` ## [1.4.2] – 2020-11-01 _Summary for the 1.4.2 release: standardized hexadecimal parsing in the CLI; fixes for Windows and mac OS; improvements to Hydro Platinum/Pro XT and Kraken X3 drivers._ Changelog since 1.4.1: ### Added - Add `Modern690Lc.downgrade_to_legacy` (unstable API) ### Changed - Accept hexadecimal inputs regardless of a `0x` prefix - Warn on faulty temperature readings from Kraken X3 coolers - Warn on Hydro Platinum/Pro XT firmware versions that are may be too old - Update PyInstaller used for the Windows executable - Update PyUSB version bundled with the Windows executable - Improve the documentation ### Fixed - Fix data path on mac OS - Only set the sticky bit for data directories on Linux - Fix check of maximum number of colors in Hydro Platinum super-fixed mode - Fix HID writes to Corsair HXi/RMi power supplies on Windows - Ensure Hydro Platinum/Pro XT is in static LEDs hardware mode ### Checksums ``` 83517ccb06cfdda556bc585a6a45edfcb5a21e38dbe270454ac97639d463e96d dist/liquidctl-1.4.2-bin-windows-x86_64.zip 39da5f5bcae1cbd91e42e78fdb19f4f03b6c1a585addc0b268e0c468e76f1a3c dist/liquidctl-1.4.2.tar.gz ``` ## [1.4.1] – 2020-08-07 _Summary for the 1.4.1 release: fix a regression with NZXT E-series PSUs, an unreliable test case, and some ignored Hidapi errors; also make a few other small improvements to the documentation and test suite._ Changelog since 1.4.0: ### Changed - Improve the documentation - Improve the test suite ### Fixed - Don't use report IDs when writing to NZXT E-series PSUs (#166) - Recognize and raise Hidapi write errors - Use a mocked device to test backwards compatibility with liquidctl 1.1.0 ### Checksums ``` 895e55fd70e1fdfe3b2941d9139b91ffc4e902a469b077e810c35979dbe1cfdf liquidctl-1.4.1-bin-windows-x86_64.zip 59a3bc65b3f3e71a5714224401fe6e95dfdee591a1d6f4392bc4e6d6ad72ff8d liquidctl-1.4.1.tar.gz ``` ## [1.4.0] – 2020-07-31 _Summary for the 1.4.0 release: fourth-generation NZXT Kraken coolers, Corsair Platinum and Pro XT coolers, select Gigabyte RGB Fusion 2.0 motherboards, additional color formats, improved fan and pump profiles in third-generation Krakens, and other improvements._ Changelog since 1.3.3: ### Added - Add experimental support for NZXT Kraken X53, X63 and X73 coolers - Add experimental partial support for NZXT Kraken Z63 and Z73 coolers - Add experimental support for Corsair H100i, H100i SE and H115i Platinum coolers - Add experimental partial support for Corsair H100i and H115i Pro XT coolers - Add experimental support for Gigabyte motherboards with RGB Fusion 2.0 5702 and 8297 controllers - Enable experimental support for the NZXT RGB & Fan Controller - Add support for HSV, HSL and explicit RGB color representations - Add `sync` lighting channel to HUE 2 devices - Add tentative names for the different +12 V rails of NZXT E-series PSUs - Add +uaccess udev rules for Linux distributions and users - Add `--pump-mode` option to `initialize` (Corsair Platinum/Pro XT coolers) - Add `--unsafe` option to enable additional bleeding-edge features - Add a test suite - [extra] Add more general `yoda` script for software-based fan/pump control (supersedes `krakencurve-poc`) ### Changed - Increase resolution of fan and pump profiles in Kraken X42/X52/X62/X72 coolers - Use hidapi to communicate with HIDs on Windows - Use specific errors when features are not supported by the device or the driver - Store runtime data on non-Linux systems in `~/Library/Caches` (macOS), `%TEMP%` (Windows) or `/tmp` (Unix) - Mark Corsair HXi/RMi PSUs as no longer experimental - Mark Smart Device V2 and HUE 2 controllers as no longer experimental - Switch to a consistent module, driver and guide naming scheme (aliases are kept for backwards compatibility) - Improve the documentation - [extra] Refresh `krakencurve-poc` syntax and sensor names, and get CPU temperature on macOS with iStats ### Fixed - Add missing identifiers for some HUE2 accessories (#95; #109) - Fix CAM-like decoding of firmware version in NZXT E-series PSUs (#46, comment) - Use a bitmask to select the lighting channel in HUE 2 devices (#109) - Close the underlying cython-hidapi `device` - Don't allow `HidapiDevice.clear_enqueued_reports` to block - Don't allow `HidapiDevice.address` to fail with non-Unicode paths - Store each runtime data value atomically ### Deprecated - Deprecate and ignore `--hid` override for API selection ### Removed - Remove the PyUsbHid device backend for HIDs ### Checksums ``` 250b7665b19b0c5d9ae172cb162bc920734eba720f3e337eb84409077c582966 liquidctl-1.4.0-bin-windows-x86_64.zip b35e6f297e67f9e145794bb57b88c626ef2bfd97e7fbb5b098f3dbf9ae11213e liquidctl-1.4.0.tar.gz ``` ## [1.3.3] – 2020-02-18 _Summary for the 1.3.3 release: fix possibly stale data with HIDs and other minor issues._ Changelog since 1.3.2: ### Fixed - Add missing identifiers for HUE+ accessories on HUE 2 channels - Forward hid argument from `UsbHidDriver.find_supported_devices` - Prevent reporting stale data during long lived connections to HIDs (#87) ### Checksums ``` 1422a892f9c2c69f5949cd831083c6fef8f6a1f6e3215e90b696bfcd557924b4 liquidctl-1.3.3-bin-windows-x86_64.zip d13180867e07420c5890fe1110e8f45fe343794549a9ed7d5e8e76663bc10c24 liquidctl-1.3.3.tar.gz ``` ## [1.3.2] – 2019-12-11 _Summary for the 1.3.2 release: fix fan status reporting from Smart Device V2._ Changelog since 1.3.1: ### Fixed - Parse Smart Device V2 fan info from correct status message ### Checksums ``` acf44a491567703c109c03f446c3c0761e5f9b97098613f8ecb4366a1d2afd50 liquidctl-1.3.2-bin-windows-x86_64.zip bb742947c15f4a3987685641c0dd73184c4a40add5ad818ced68e5ace3631b6b liquidctl-1.3.2.tar.gz ``` ## [1.3.1] – 2019-11-23 _Summary for the 1.3.1 release: fix parsing of `--verbose` and documentation improvements._ Changelog since 1.3.0: ### Changed - List included dependencies and versions in Windows' bundle - Improve the documentation ### Fixed - Fix parsing of `--verbose` in commands other than `list` ### Checksums ``` de272dad305dc6651265640a280bedb21bc680a62117e625004c6aad2104da63 liquidctl-1.3.1-bin-windows-x86_64.zip 6092a6fae477908c80adc825b290e39f0b26e604593884da23d40e892e553309 liquidctl-1.3.1.tar.gz ``` ## [1.3.0] – 2019-11-17 _Summary for the 1.3.0 release: man page, Corsair RXi/HXi and NZXT E power supplies, Smart Device V2 and HUE 2 family, improved device discovery and selection._ Changelog since 1.3.0rc1: ### Added - Enable experimental support for the NZXT HUE 2 - Enable experimental support for the NZXT HUE 2 Ambient - Add `-m, --match ` to allow filtering devices by description - Add `-n` short alias for `--pick` ### Changed - Allow `initialize` methods to optionally return status tuples - Conform to XDG basedir spec and prefer `XDG_RUNTIME_DIR` - Improve directory names for internal data - Ship patched PyUSB and libusb 1.0.22 on Windows - Improve the documentation ### Fixed - Release the USB interface of NZXT E-series PSUs as soon as possible - Fix assertion in retry loops with NZXT E-series PSUs - Fix LED blinking when executing `status` on a Smart Device V2 - Add missing identifier for 250 mm HUE 2 LED strips - Restore experimental tag for the NZXT Kraken X31/X41/X61 family ### Removed - Remove dependency on appdirs ### Checksums ``` ff935fd3d57dead4d5218e02f834a825893bc6716f96fc9566a8e3989a7c19fe liquidctl-1.3.0-bin-windows-x86_64.zip ce0483b0a7f9cf2618cb30bdf3ff4195e20d9df6c615f69afe127f54956e42ce liquidctl-1.3.0.tar.gz ``` ## [1.3.0rc1] – 2019-11-03 Changelog since 1.2.0: ### Added - Add experimental support for Corsair HX750i, HX850i, HX1000i and HX1200i power supplies - Add experimental support for Corsair RM650i, RM750i, RM850i and RM1000i power supplies - Add experimental support for NZXT E500, E650 and E850 power supplies - Add experimental support for the NZXT Smart Device V2 - Add liquidctl(8) man page - Add `initialize all` variant/helper - Add `--pick ` device selection option - Add `--single-12v-ocp` option to `initialize` (Corsair HXi/RMi PSUs) ### Changed - Reduce the number of libusb and hidapi calls during device discovery - Improve the visual hierarchy of the output `list` and `status` - Allow `list --verbose` to run without root privileges (Linux) or special drivers (Windows) - Change the default API for HIDs on Linux to hidraw - Consider stable: Corsair H80i v2, H100i v2, H115i; NZXT Kraken X31, X41, X61; NZXT Grid+ V3 ### Fixed - Don't try to reattach the kernel driver more than once - Fixed Corsair H80i GT device name throughout the program - Fixed Corsair H100i GT device name in listing ### Deprecated - Use `liquidctl.driver.find_liquidctl_devices` instead of `liquidctl.cli.find_all_supported_devices` ### Checksums ``` $ sha256sum liquidctl-1.3.0rc1* 7a16a511baf5090c34cd3dfc5c21068a298515f31315be63e9b991ea17654671 liquidctl-1.3.0rc1-bin-windows-x86_64.zip 1ef517ba33e366167f9a225c6a6afcc4899d01cbd7853bd5852ac15ae81d5005 liquidctl-1.3.0rc1-py3-none-any.whl 15583d6ebecad722e1562164cef7097a358d6a57aa33a1a5e25741690548dbfa liquidctl-1.3.0rc1.tar.gz ``` ## [1.2.0] – 2019-09-27 _Summary for the 1.2.0 release: support for Asetek "5-th gen." 690LC coolers and improvements for HIDs and Mac OS._ Changelog since 1.2.0rc4: ### Changed - Include extended version information in pre-built executables for Windows ### Fixed - Improve handling of USB devices with no active configuration ## [1.2.0rc4] – 2019-09-18 Changelog since 1.2.0rc3: ### Added - Add support for adding git commit and tree cleanliness information to `--version` - Add support for adding distribution name and package information to `--version` ### Changed - Enable modern features for all Asetek 690LC coolers from Corsair - Include version information in `--debug` - Make docs and code consistent on which devices are only experimentally supported - Revert "Mark Kraken X31, X41, X51 and X61 as no longer experimental" - Improve the documentation ## [1.2.0rc3] – 2019-09-15 Changelog since 1.2.0rc2: ### Added - [extra] Add experimental `liquiddump` script ### Changed - Copy documentation for EVGA and Corsair 690LC coolers into the tree - Use modern driver with fan profiles for Corsair H115i (#41) - Claim the interface proactively when starting a transaction on any Asetek 690LC (#42) ### Fixed - Rework USBXPRESS flow control in Asetek 690LC devices to allow simultaneous reads from multiple processes (#42) - Fix missing argument forwarding to legacy Asetek 690LC coolers - Fix broken link to Mac OS example configuration ## [1.2.0rc2] – 2019-09-12 Changelog since 1.2.0rc1: ### Added - Support the EVGA CLC 360 - Add `--alert-threshold` and `--alert-color` ### Changed - Mark Kraken X31, X41, X51 and X61 as no longer experimental - Improve supported devices list and links to documentation - Don't enable PyUSB tracing automatically with `--debug` - Cache values read from or stored on the filesystem - Prefer to save driver data in /run when OS is Linux ### Fixes - Force bundling of `hid` module in Windows executable - Change default Asetek 690LC `--time-per-color` for fading mode (#29) ## [1.2.0rc1] – 2019-04-14 Changelog since 1.1.0: ### Added - Add support for EVGA CLC 120 CL12, 240 and 280 coolers - Add experimental support for NZXT Kraken X31, X41 and X61 coolers - Add experimental support for Corsair H80i v2, H100i v2 and H115i - Add experimental support for Corsair H80i GT, H100i GTX and H110i GTX - Add support for macOS - Make automatic bundled builds for Windows with AppVeyor - Add support for hidapi for HIDs (default/required on macOS) - Add release number, bus and address listing - Add `sync` pseudo channel for setting all Smart Device/Grid+ V3 fans at once - Add `--hid ` override for HID API selection - Add `--release`, `--bus`, `--address` device filters - Add `--time-per-color` and `--time-off` animation options - Add `--legacy-690lc` option for Asetek 690LC devices - Document possible support of NZXT Kraken X40 and X60 coolers ### Changed - Revamp driver and device model in `liquidctl.driver.{base,usb}` modules ### Removed - Remove `--dry-run` ## [1.1.0] – 2018-12-15 _Summary for the 1.1.0 release: support for NZXT Smart Device, Grid+ V3 and Kraken M22._ Changelog since 1.1.0rc1: ### Added - [extra] Add proof of concept `krakencurve-poc` script for software-based speed control ### Changed - Change Kraken M22 from experimental to implemented - Only show exception tracebacks if -g has been set - Improve the documentation ### Fixes - Use standard NotImplementedError exception ## [1.1.0rc1] - 2018-11-14 Changelog since 1.0.0: ### Added - Add support for the NZXT Smart Device - Add experimental support for the NZXT Grid+ V3 - Add experimental support for the NZXT Kraken M22 - Add `initialize` command for the NZXT Smart Device, NZXT Grid+ V3 and similar products - Add device filtering options: `--vendor`, `--product`, `--usb-port` and `--serial` - Add `super-breathing`, `super-wave` and `backwards-super-wave` modes for Krakens - Add `--debug` to complement `--verbose` - Add special Kraken `set_instantaneous_speed(channel, speed)` API - Expose Kraken `supports_lighting`, `supports_cooling` and `supports_cooling_profiles` properties - [extra] Add proof of concept `krakenduty-poc` script for status-duty translation ### Changed - Lower the minimum pump duty to 50% - No longer imply `--verbose` from `--dry-run` - Improve the API for external code that uses our drivers - Switch to the standard Python `logging` module - Improve the documentation ### Fixes - Fix standalone module entry point for the CLI - [Kraken] Fix fan and pump speed configuration on firmware v2.1.8 or older ### Deprecated - [Kraken] Deprecate `super`; use `super-fixed` instead - [Kraken] Deprecate undocumented API behavior of `initialize()` and `finalize()`; use `connect()` and `disconnect()` instead ### Removed - Remove unused symbols in `liquidctl.util` ## [1.0.0] - 2018-08-31 _Summary for the 1.0.0 release: support for NZXT Kraken X42/X52/X62/X72 coolers._ Changelog since 1.0.0rc1: ### Added - Add helper color mode: `off` - Add backward variant of `moving-alternating` color mode ### Changed - Improve the documentation - Allow covering marquees with only one color ### Fixes - Fix mentions to incorrect Kraken generation - Correct the modifier byte for the `moving-alternating` mode ## [1.0.0rc1] - 2018-08-26 ### Added - Add driver for NZXT Kraken X42, X52, X62 and X72 coolers ## About the changelog All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) and [PEP 404](https://www.python.org/dev/peps/pep-0440/#semantic-versioning). liquidctl-1.5.1/LICENSE.txt000066400000000000000000001045151401367561700153530ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . liquidctl-1.5.1/MANIFEST.in000066400000000000000000000002351401367561700152600ustar00rootroot00000000000000include CHANGELOG.md include LICENSE.txt include liquidctl.8 include pytest.ini recursive-include docs * recursive-include extra * recursive-include tests * liquidctl-1.5.1/README.md000066400000000000000000000613201401367561700150030ustar00rootroot00000000000000# liquidctl – liquid cooler control _Cross-platform tool and drivers for liquid coolers and other devices_ [![Status of the tests](https://github.com/liquidctl/liquidctl/workflows/tests/badge.svg)](https://github.com/liquidctl/liquidctl/commits/master) [![Status of the build for Windows](https://ci.appveyor.com/api/projects/status/n5lgebd5m8iomx42/branch/master?svg=true)](https://ci.appveyor.com/project/jonasmalacofilho/liquidctl/branch/master) [![Status of Linux packaging](https://repology.org/badge/tiny-repos/liquidctl.svg?header=linux%20distros)](#installing-on-linux) ``` # liquidctl list Device ID 0: NZXT Smart Device (V1) Device ID 1: NZXT Kraken X (X42, X52, X62 or X72) # liquidctl initialize all # liquidctl --match kraken set fan speed 20 30 30 50 34 80 40 90 50 100 # liquidctl --match kraken set sync color spectrum-wave # liquidctl --match smart set led color fading 350017 ff2608 # liquidctl --match smart set fan1 speed 50 # liquidctl status NZXT Smart Device (V1) ├── Fan 1 PWM ├── Fan 1 current 0.04 A ├── Fan 1 speed 1035 rpm ├── Fan 1 voltage 11.91 V ├── Fan 2 — ├── Fan 3 — ├── Firmware version 1.0.7 ├── LED accessories 2 ├── LED accessory type HUE+ Strip ├── LED count (total) 20 └── Noise level 60 dB NZXT Kraken X (X42, X52, X62 or X72) ├── Liquid temperature 28.1 °C ├── Fan speed 851 rpm ├── Pump speed 1953 rpm └── Firmware version 6.0.2 ``` ## Table of contents 1. [Supported devices](#supported-devices) 2. [Installing on Linux](#installing-on-linux) 3. [Installing on Windows](#installing-on-windows) 4. [Installing on macOS](#installing-on-macos) 5. [The command-line interface](#introducing-the-command-line-interface) 1. [Listing and selecting devices](#listing-and-selecting-devices) 2. [Initializing and interacting with devices](#initializing-and-interacting-with-devices) 3. [Supported color specification formats](#supported-color-specification-formats) 6. [Automation and running at boot](#automation-and-running-at-boot) 1. [Set up Linux using systemd](#set-up-linux-using-systemd) 2. [Set up Windows using Task Scheduler](#set-up-windows-using-task-scheduler) 3. [Set up macOS using launchd](#set-up-macos-using-launchd) 7. [Troubleshooting](#troubleshooting) 8. [Additional documentation](#additional-documentation) 9. [License](#license) 10. [Related projects](#related-projects-2020-edition) ## Supported devices The following devices are supported by this version of liquidctl. See each guide for specific usage instructions and other pertinent information. | Type | Device/guide | Bus | Notes | | :-: | :-- | :-: | :-- | | AIO liquid cooler | [Corsair Hydro H100i Pro XT, H115i Pro XT](docs/corsair-platinum-pro-xt-guide.md) | USB HID | _EU_ | | AIO liquid cooler | [Corsair Hydro H100i Platinum [SE], H115i Platinum](docs/corsair-platinum-pro-xt-guide.md) | USB HID | _E_ | | AIO liquid cooler | [Corsair Hydro H80i GT, H100i GTX, H110i GTX](docs/asetek-690lc-guide.md) | USB | _ZE_ | | AIO liquid cooler | [Corsair Hydro H80i v2, H100i v2, H115i](docs/asetek-690lc-guide.md) | USB | _Z_ | | AIO liquid cooler | [EVGA CLC 120 (CL12), 240, 280, 360](docs/asetek-690lc-guide.md) | USB | _Z_ | | AIO liquid cooler | [NZXT Kraken M22](docs/kraken-x2-m2-guide.md) | USB HID | | | AIO liquid cooler | [NZXT Kraken X31, X41, X61](docs/asetek-690lc-guide.md) | USB | _LZE_ | | AIO liquid cooler | [NZXT Kraken X40, X60](docs/asetek-690lc-guide.md) | USB | _LZE_ | | AIO liquid cooler | [NZXT Kraken X42, X52, X62, X72](docs/kraken-x2-m2-guide.md) | USB HID | | | AIO liquid cooler | [NZXT Kraken X53, X63, X73](docs/kraken-x3-z3-guide.md) | USB HID | | | AIO liquid cooler | [NZXT Kraken Z63, Z73](docs/kraken-x3-z3-guide.md) | USB & USB HID | _E_ | | DDR4 DRAM | [Corsair Vengeance RGB](docs/ddr4-guide.md) | SMBus | _EUX_ | | DDR4 DRAM | [DIMMs with a standard temperature sensor](docs/ddr4-guide.md) | SMBus | _EUX_ | | Fan/LED controller | [Corsair Commander Pro](docs/corsair-commander-guide.md) | USB HID | _E_ | | Fan/LED controller | [Corsair Lighting Node Pro](docs/corsair-commander-guide.md) | USB HID | _E_ | | Fan/LED controller | [NZXT Grid+ V3](docs/nzxt-smart-device-v1-guide.md) | USB HID | | | Fan/LED controller | [NZXT HUE 2, HUE 2 Ambient](docs/nzxt-hue2-guide.md) | USB HID | | | Fan/LED controller | [NZXT RGB & Fan Controller](docs/nzxt-hue2-guide.md) | USB HID | | | Fan/LED controller | [NZXT Smart Device](docs/nzxt-smart-device-v1-guide.md) | USB HID | | | Fan/LED controller | [NZXT Smart Device V2](docs/nzxt-hue2-guide.md) | USB HID | | | Graphics card | [ASUS Strix RTX 2080 Ti OC](docs/nvidia-guide.md) | I²C | _UX_ | | Graphics card | [EVGA GTX 1080 FTW](docs/nvidia-guide.md) | I²C | _UX_ | | Motherboard | [Gigabyte RGB Fusion 2.0 motherboards](docs/gigabyte-rgb-fusion2-guide.md) | USB HID | | | Power supply | [Corsair HX750i, HX850i, HX1000i, HX1200i](docs/corsair-hxi-rmi-psu-guide.md) | USB HID | | | Power supply | [Corsair RM650i, RM750i, RM850i, RM1000i](docs/corsair-hxi-rmi-psu-guide.md) | USB HID | | | Power supply | [NZXT E500, E650, E850](docs/nzxt-e-series-psu-guide.md) | USB HID | _E_ | _L_ _Requires the `--legacy-690lc` flag._ _Z_ _Requires replacing the device driver [on Windows](#installing-on-windows)._ _E_ _Experimental and/or partial support._ _U_ _Requires `--unsafe` features._ _X_ _Only supported on Linux._ _N_ _New driver, only available on git._ ## Installing on Linux Packaging status Packages are available for some Linux distributions. On others, or when more control is desired, liquidctl can be installed from PyPI or directly from the source code repository. The following dependencies are required at runtime (common package names are listed in parenthesis): - Python 3.6+ _(python3, python)_ - pkg_resources Python package _(python3-setuptools, python3-pkg-resources, python-setuptools)_ - docopt _(python3-docopt, python-docopt)_ - cython-hidapi _(python3-hidapi, python3-hid, python-hidapi)_ - PyUSB _(python3-pyusb, python3-usb, python-pyusb)_ - LibUSB 1.0 _(libusb-1.0, libusb-1.0-0, libusbx)_ - smbus Python package _(python3-i2c-tools, python3-smbus, i2c-tools)_ To locally test and manually install, a few more dependencies are needed: - setuptools Python package _(python3-setuptools, python-setuptools)_ - pip (optional) _(python3-pip, python-pip)_ - pytest (optional) _(python3-pytest, pytest, python-pytest)_ Finally, if cython-hidapi will be installed from source or directly from PyPI, then some additional build tools and development headers may also be required: - Python development headers _(python3-dev, python3-devel)_ - LibUSB 1.0 development headers _(libusb-1.0-0-dev, libusbx-devel)_ - libudev developemnt headers _(libudev-dev, libudev-devel)_ Once all necessary dependencies are installed, *pip* can be used to install a release from PyPI: ``` # pip install liquidctl # pip install liquidctl== ``` For the latest changes and to contribute back to the project, it is best to clone the source code repository. You can directly execute the code, or install it from that local copy. ``` $ git clone https://github.com/liquidctl/liquidctl $ cd liquidctl $ pytest # optional step $ python -m liquidctl.cli ... # pip install . ``` _Note: in systems that default to Python 2, use `pip3`, `python3` and `pytest-3`._ Optional steps: - install man pages ``` # cp liquidctl.8 /usr/local/share/man/man8/ # mandb ``` - install [udev rules] for unprivileged access to devices - install [modules-load configuration] for SMBus/I²C support - install [bash completions] for liquidctl [udev rules]: extra/linux/71-liquidctl.rules [modules-load configuration]: extra/linux/modules-load.conf [bash completions]: extra/completions/liquidctl.bash ## Installing on Windows A pre-built executable for the last stable version is available in [liquidctl-1.5.1-bin-windows-x86_64.zip](https://github.com/liquidctl/liquidctl/releases/download/v1.5.1/liquidctl-1.5.1-bin-windows-x86_64.zip). Executables for previous releases can be found in the assets of the [Releases](https://github.com/liquidctl/liquidctl/releases) tab, and development builds can be found in the artifacts on the [AppVeyor runs](https://ci.appveyor.com/project/jonasmalacofilho/liquidctl/history). Products that are not Human Interface Devices (HIDs), or that do not use the Microsoft HID Driver, require a libusb-compatible driver, see notes in [Supported devices](#supported-devices)). In most cases Microsoft WinUSB is recommended, which can easily be set up for a device with [Zadig](https://zadig.akeo.ie/)¹: open the application, click `Options`, `List All Devices`, then select your device from the dropdown list, and click "Replace Driver". Note that replacing the driver for devices that do not require it will likely cause them to disapear from liquidctl. The pre-built executables can be directly used from a Windows Command Prompt, Power Shell or other available terminal emulator. Even so, most users will want to place the executable in a directory listed in [the `PATH` environment variable](https://en.wikipedia.org/wiki/PATH_(variable)), or change the variable so that is true; this allows omitting the full path and `.exe` extension when calling `liquidctl`. _Alternatively to the pre-built executable,_ it is possible to install liquidctl from PyPI or directly from the source code repository. This is useful to contribute fixes or improvements to liquidctl, or to use advanced features like the liquidctl API. Since HWiNFO 6.10 it is possible for other programs to send additional sensor data in through a Windows Registry API, and [`LQiNFO.py`](extra/windows/LQiNFO.py) is an experimental program that uses the liquidctl API to take advantage of this feature. Pre-build liquidctl executables for Windows already include Python and libusb, but when installing from PyPI or the sources both of these will need to be manually set up. The libusb DLLs can be found in [libusb/releases](https://github.com/libusb/libusb/releases) (part of the `libusb-.7z` files) and the appropriate (e.g. MS64) `.dll` and `.lib` files should be extracted to the system or python installation directory (e.g. `C:\Windows\System32` or `C:\Python36`). To install any release from PyPI, *pip* should be used: ``` > pip install liquidctl > pip install liquidctl== ``` For the latest changes and to contribute back to the project, it is best to clone the source code repository. You can directly execute the code, or install it from that local copy. ``` > git clone https://github.com/liquidctl/liquidctl > cd liquidctl > python -m liquidctl.cli ... > pip install . ``` _¹ See [How to use libusb under Windows](https://github.com/libusb/libusb/wiki/FAQ#how-to-use-libusb-under-windows) for more information._ ## Installing on macOS liquidctl is available on Homebrew, and that is the preferred method of installing it. ``` $ brew install liquidctl $ brew install liquidctl --HEAD ``` By default the last stable version will be installed, but by passing `--HEAD` this can be changed to the last snapshot from this repository. All dependencies are automatically resolved. Another possibility is to install liquidctl from PyPI or directly from the source code repository, but in these cases Python 3 and libsub must be installed first; the recommended way is with `brew install python libusb`. To install any release from PyPI, *pip* should be used: ``` $ pip3 install liquidctl $ pip3 install liquidctl== ``` For the latest changes and to contribute back to the project, it is best to clone the source code repository. You can directly execute the code, or install it from that local copy. ``` $ git clone https://github.com/liquidctl/liquidctl $ cd liquidctl $ python3 -m liquidctl.cli ... $ pip3 install . ``` _Note: installation into a virtual environment is recommended to avoid conflicts with Python modules installed with Homebrew. The use of virtual environments is outside the scope of this document. Their use will also restrict the availability of the liquidctl command to that virtual environment._ ## Introducing the command-line interface The complete list of commands and options can be found in `liquidctl --help` and in the man page, but the following topics cover the most common operations. Brackets `[ ]`, parenthesis `( )`, less than/greater than `< >` and ellipsis `...` are used to describe, respectively, optional, required, positional and repeating elements. Example commands are prefixed with a number sign `#`, which also serves to indicate that on Linux root permissions (or suitable udev rules) may be required. The `--verbose` option will print some extra information, like automatically made adjustments to user-provided settings. And if there is a problem, the `--debug` flag will make liquidctl output more information to help identify its cause; be sure to include this when opening a new issue. _Note: when debugging issues with PyUSB or libusb it can be useful to set the `PYUSB_DEBUG=debug` or/and `LIBUSB_DEBUG=4` environment variables._ ### Listing and selecting devices A good place to start is to ask liquidctl to list all recognized devices. ``` # liquidctl list Device ID 0: NZXT Smart Device (V1) Device ID 1: NZXT Kraken X (X42, X52, X62 or X72) ``` In case more than one supported device is found, one them can be selected with `--match `, where `` matches part of the desired device's description using a case insensitive comparison. ``` # liquidctl --match kraken list Device ID 0: NZXT Kraken X (X42, X52, X62 or X72) ``` More device properties can be show by passing `--verbose` to `liquidctl list`. Any of these can also be used to select a particular product. ``` # liquidctl --serial 1234567890 list Device ID 0: NZXT Kraken X (X42, X52, X62 or X72) ``` Ambiguities for any given filter can be solved with `--pick `. Devices can also be selected with `--device `, but these IDs are not guaranteed to remain stable and will vary with hardware changes, liquidctl updates or simply normal variance in enumeration order. ### Initializing and interacting with devices Devices will usually need to be initialized before they can be used, though each device has its own requirements and limitations. This and other information specific to a particular device will appear on the documentation linked from the [supported devices](#supported-devices) section. Devices can be initialized individually or all at once. ``` # liquidctl [options] initialize [all] ``` Most devices provide some status information, like fan speeds and liquid temperatures. This can be queried for all devices or using the filtering methods mentioned before. ``` # liquidctl [options] status ``` Fan and pump speeds can be set to fixed values or, if the device supports them, custom profiles. ``` # liquidctl [options] set speed ( ) ... # liquidctl [options] set speed ``` Lighting is controlled in a similar fashion. The specific documentation for each device will list the available channels, modes and additional options. ``` # liquidctl [options] set color [] ... ``` ### Supported color specification formats When configuring lighting effects, colors can be specified in different representations and formats: - as an implicit hexadecimal RGB triple, either with or without the `0x` prefix: e.g. `ff7f3f` - as an explicit RGB triple: e.g. `rgb(255, 127, 63)` - as a HSV (hue‑saturation‑value) triple: e.g. `hsv(20, 75, 100)` * hue ∊ [0, 360] (degrees); saturation, value ∊ [0, 100] (percent) * note: this is sometimes called HSB (hue‑saturation‑brightness) - as a HSL (hue‑saturation‑lightness) triple: e.g. `hsl(20, 100, 62)` * hue ∊ [0, 360] (degrees); saturation, lightness ∊ [0, 100] (percent) Color arguments containing spaces, parenthesis or commas need to be quoted, as these characters can have special meaning on the command-line; the easiest way to do this on all supported platforms is with double quotes. ``` # liquidctl --match kraken set ring color fading "hsv(0,80,100)" "hsv(180,80,100)" ``` On Linux it is also possible to use single-quotes and `\(`, `\)`, `\ ` escape sequences. ## Automation and running at boot In most cases you will want to automatically apply your settings when the system boots. Generally a simple script or a basic service is enough, and some specifics about this are given in the following sections. For even more flexibility, you can also write a Python program that calls the driver APIs directly. ### Set up Linux using systemd On systems running Linux and systemd a service unit can be used to configure liquidctl devices. A simple example is provided bellow, which you can edit to match your preferences. Save it to `/etc/systemd/system/liquidcfg.service`. ``` [Unit] Description=AIO startup service [Service] Type=oneshot ExecStart=liquidctl set pump speed 90 ExecStart=liquidctl set fan speed 20 30 30 50 34 80 40 90 50 100 ExecStart=liquidctl set ring color fading 350017 ff2608 ExecStart=liquidctl set logo color spectrum-wave [Install] WantedBy=default.target ``` After reloading the configuration, the new unit can be started manually or set to automatically run during boot using standard systemd tools. ``` # systemctl daemon-reload # systemctl start liquidcfg # systemctl enable liquidcfg ``` A slightly more complex example can be seen at [jonasmalacofilho/dotfiles](https://github.com/jonasmalacofilho/dotfiles/tree/master/liquidctl), which includes dynamic adjustments of the lighting depending on the time of day. If necessary, it is also possible to have the service unit explicitly wait for the device to be available: see [making systemd units wait for devices](docs/linux/making-systemd-units-wait-for-devices). ### Set up Windows using Task Scheduler The configuration of devices can be automated by writing a batch file and setting up a new task for (every) login using Windows Task Scheduler. The batch file can be really simple and only needs to contain the invocations of liquidctl that would otherwise be done manually. ```batchfile liquidctl set pump speed 90 liquidctl set fan speed 20 30 30 50 34 80 40 90 50 100 liquidctl set ring color fading 350017 ff2608 liquidctl set logo color spectrum-wave ``` Make sure that liquidctl is available in the context where the batch file will run: in short, `liquidctl --version` should work within a _normal_ Command Prompt window. When not using a pre-built liquidctl executable, try installing Python with the option to set the PATH variable enabled, or manually add the necessary folders to the PATH. A slightly more complex example can be seen in [issue #14](https://github.com/liquidctl/liquidctl/issues/14#issuecomment-456519098) ("Can I autostart liquidctl on Windows?"), that uses the LEDs to convey progress or eventual errors. Chris' guide on [Replacing NZXT’s CAM software on Windows for Kraken](https://codecalamity.com/replacing-nzxts-cam-software-on-windows-for-kraken/) is also a good read. As an alternative to using Task Scheduler, the batch file can simply be placed in the startup folder; you can run `shell:startup` to [find out where that is](https://support.microsoft.com/en-us/help/4026268/windows-10-change-startup-apps). ### Set up macOS using launchd You can use a shell script and launchd to automatically configure your devices during login or after waking from sleep. A [detailed guide](https://www.tonymacx86.com/threads/gigabyte-z490-vision-d-thunderbolt-3-i5-10400-amd-rx-580.298642/page-24#post-2138475) is available on tonymacx86. ## Troubleshooting ### Device not listed (Windows) This is likely caused by having replaced the standard driver of a USB HID. If the device in question is not marked in [Supported devices](#supported-devices) as requiring a special driver, try uninstalling the custom driver. ### Device not listed (Linux) This is usually caused by having an unexpected kernel driver bound to a USB HID. In most cases this is the result of having used a program that accessed the device (directly or indirectly) via libusb-1.0, but failed to reattach the original driver before terminating. This can be temporarily solved by manually rebinding the device to the kernel `usbhid` driver. Replace `` and `` with the correct values from `lsusb -vt` (also assumes there is only HID interface, adjust if necessary): ``` echo '-:1.0' | sudo tee /sys/bus/usb/drivers/usbhid/bind ``` A more permanent solution is to politely ask the authors of the program that is responsible for leaving the kernel driver detached to use `libusb_attach_kernel_driver` or `libusb_set_auto_detach_kernel_driver`. ### Access denied or open failed (Linux) These errors are usually caused by a lack of permission to access the device. On Linux distros that normally requires root privileges. Alternatively to running liquidctl as root (or with `sudo`), you can install the udev rules provided in [`extra/linux/71-liquidctl.rules`](extra/linux/71-liquidctl.rules) to allow unprivileged access to the devices supported by liquidctl. ### Other problems If your problem is not listed here, try searching the [issues](https://github.com/liquidctl/liquidctl/issues). If no issue matches your problem, you still need help, or you have found a bug, please open one. When commenting on an issue, please describe the problem in as much detail as possible. List your operating system and the specific devices you own. Also include the arguments and output of all relevant/failing liquidctl commands, using the `--debug` option to enable additional debug information. ## Additional documentation Be sure to browse [`docs/`](docs/) for additional documentation, and [`extra/`](extra/) for some example scripts and other possibly useful things. You are also encouraged to contribute to the documentation and to these examples, including adding new files that cover your specific use cases or solutions. ## License liquidctl – monitor and control liquid coolers and other devices. Copyright (C) 2018–2021 Jonas Malaco, Marshall Asch, CaseySJ, Tom Frey and contributors liquidctl incorporates work by leaty, Ksenija Stanojevic, Alexander Tong, Jens Neumaier, Kristóf Jakab, Sean Nelson, Chris Griffith, notaz, realies and Thomas Pircher. Depending on how it is packaged, it might also bundle copies of python, hidapi, libusb, cython-hidapi, pyusb and docopt. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . This project uses [short SPDX License List identifiers][SPDX-Short-Identifiers] to concisely and unambiguously indicate the applicable license in each source file. [SPDX-Short-Identifiers]: https://spdx.github.io/spdx-spec/appendix-V-using-SPDX-short-identifiers-in-source-files/ ## Related projects ### [CalcProgrammer1/OpenRGB](https://gitlab.com/CalcProgrammer1/OpenRGB) Open-source graphical interface to control many different types of RGB devices. ### [liquidctl/liquidtux](https://github.com/liquidctl/liquidtux) Initial conversion of liquidctl to Linux kernel _hwmon_ drivers. Currently allows standard monitoring tools (for example lm-sensors, or tools built on top of it) to read sensor data from Kraken X42/X52/X62/X72 coolers and the Smart Device (V1). ### [audiohacked/OpenCorsairLink](https://github.com/audiohacked/OpenCorsairLink) Retired in 2020, but a great source of information on how Corsair devices work. There are ongoing efforts to port the drivers to liquidctl, and joining them is a great way to get involved. ### [liquidctl/collected-device-data](https://github.com/liquidctl/collected-device-data) Device information collected for developing and maintaining liquidctl, including USB descriptors, traffic captures and protocol analyzes. liquidctl-1.5.1/appveyor.yml000066400000000000000000000052761401367561700161240ustar00rootroot00000000000000version: "{build}" skip_commits: message: /\[skip ci\]/ image: - Visual Studio 2017 platform: x64 environment: DIST_NAME: AppVeyor LIBUSB_VERSION: 1.0.23 for: - branches: only: - /v\d+\.\d+\.\d+.*/ environment: is_release: true install: # conditionally generate DIST_PACKAGE from TAG_NAME or REPO_COMMIT: # - because of AppVeyor limitation, do this here and not in environment; # - don't be sane and use 'else', Windows/Cmd/AppVeyor wont like it - cmd: if [%is_release%]==[true] set DIST_PACKAGE=liquidctl-%APPVEYOR_REPO_TAG_NAME:v=%-bin-windows-x86_64 - cmd: if [%is_release%]==[] set DIST_PACKAGE=liquidctl-%APPVEYOR_REPO_COMMIT:~0,12%-bin-windows-x86_64 - cmd: echo DIST_NAME=%DIST_NAME%; DIST_PACKAGE=%DIST_PACKAGE%; LIBUSB_VERSION=%LIBUSB_VERSION% # libusb - cmd: appveyor DownloadFile https://github.com/libusb/libusb/releases/download/v%LIBUSB_VERSION%/libusb-%LIBUSB_VERSION%.7z -FileName libusb.7z - cmd: 7z x libusb.7z # python build and package tools - cmd: C:\Python38-x64\python -m pip install --upgrade pip setuptools --ignore-installed # remaining dependencies - cmd: C:\Python38-x64\python setup.py install # generate list of packages that will be included - cmd: C:\Python38-x64\python -m pip freeze | find /V "virtualenv" > extra\windows\included.txt # install pyinstaller last to not pollute included.txt with its dependencies - cmd: C:\Python38-x64\python -m pip install pyinstaller - cmd: C:\Python38-x64\python -m pip freeze | find "pyinstaller" >> extra\windows\included.txt - cmd: echo libusb %LIBUSB_VERSION% >> extra\windows\included.txt - cmd: C:\Python38-x64\python --version >> extra\windows\included.txt # run unit tests and module doctests - cmd: C:\Python38-x64\python -m pip install pytest - cmd: C:\Python38-x64\python -m pytest build_script: - cmd: C:\Python38-x64\Scripts\pyinstaller --add-data MS64\dll\*;. --clean -F liquidctl\cli.py --name liquidctl --onefile --distpath . after_build: - cmd: mkdir %DIST_PACKAGE%\docs - cmd: mkdir %DIST_PACKAGE%\docs\windows - cmd: copy liquidctl.exe %DIST_PACKAGE%\ - cmd: copy README.md %DIST_PACKAGE%\ - cmd: copy docs\*.md %DIST_PACKAGE%\docs\ - cmd: copy docs\windows\*.md %DIST_PACKAGE%\docs\windows\ - cmd: copy LICENSE.txt %DIST_PACKAGE%\ - cmd: copy extra\windows\redist-notices.txt %DIST_PACKAGE%\COPYRIGHT.txt - cmd: copy extra\windows\included.txt %DIST_PACKAGE%\included.txt - cmd: copy CHANGELOG.md %DIST_PACKAGE%\ - cmd: 7z a %DIST_PACKAGE%.zip %DIST_PACKAGE%\ - cmd: CertUtil -hashfile liquidctl.exe SHA256 - cmd: CertUtil -hashfile %DIST_PACKAGE%.zip SHA256 test_script: - cmd: liquidctl.exe list --verbose --debug artifacts: - path: "*.zip" liquidctl-1.5.1/conftest.py000066400000000000000000000003321401367561700157170ustar00rootroot00000000000000import sys collect_ignore = ['setup.py'] if sys.platform != 'linux': collect_ignore.append('tests/test_smbus.py') if sys.platform not in ['win32', 'cygwin']: collect_ignore.append('extra/windows/LQiNFO.py') liquidctl-1.5.1/docs/000077500000000000000000000000001401367561700144525ustar00rootroot00000000000000liquidctl-1.5.1/docs/asetek-690lc-guide.md000066400000000000000000000067231401367561700202060ustar00rootroot00000000000000# Asetek 690LC liquid coolers _Driver API and source code available in [`liquidctl.driver.asetek`](../liquidctl/driver/asetek.py)._ Several products are available that are based on the same Asetek 690LC base design: - Current models: * EVGA CLC 120 (CLC12), 240, 280 and 360 * Corsair Hydro H80i v2, H100i v2 and H115i * Corsair Hydro H80i GT, H100i GTX and H110i GTX - Legacy designs: * NZXT Kraken X40, X60, X31, X41, X51 and X61 **Note: a custom kernel driver is necessary on Windows (see: [Installing on Windows](../README.md#installing-on-windows)).** **Note: when dealing with legacy Krakens the `--legacy-690lc` flag should be supplied on all invocations of liquidctl.** ## Initialization All 690LC devices must be initialized sometime after the system boots. Only then it will be possible to query the device status and perform other operations. ``` # liquidctl initialize ``` ## Device monitoring Similarly to other AIOs, the cooler can report fan and pump speeds as well as the liquid temperature. ``` # liquidctl status Asetek 690LC (assuming EVGA CLC) ├── Liquid temperature 28.7 °C ├── Fan speed 480 rpm ├── Pump speed 1890 rpm └── Firmware version 2.10.0.0 ``` ## Fan and pump speed control Fan speeds can be configured either to fixed duty values or profiles. The profiles accept up to six (liquid temperature, duty) points, and are interpolated by the device. ``` # liquidctl set fan speed 50 # liquidctl set fan speed 20 0 40 100 ``` *Note: fan speed profiles are only supported on non-legacy models.* Pump speeds, on the other hand, only accept fixed duty values. ``` # liquidctl set pump speed 75 ``` ## Lighting modes There's a single lighting channel `logo`. The first light mode – 'rainbow' – supports an abstract `--speed` parameter, varying from 1 to 6. ``` # liquidctl set logo color rainbow # liquidctl set logo color rainbow --speed 1 # liquidctl set logo color rainbow --speed 6 ``` *Note: the 'rainbow' lighting mode is currently only supported by EVGA units.* The 'fading' mode supports specifying the `--time-per-color` in seconds. The defaults are 1 and 5 seconds per color for, respectively, modern and legacy coolers. ``` # liquidctl set logo color fading ff8000 00ff80 # liquidctl set logo color fading ff8000 00ff80 --time-per-color 2 ``` The 'blinking' mode accepts both `--time-per-color` and `--time-off` (also in seconds). The default is 1 second for each, and whenever unspecified `--time-off` will equal `--time-per-color`. ``` # liquidctl set logo color blinking 8000ff # liquidctl set logo color blinking 8000ff --time-off 2 # liquidctl set logo color blinking 8000ff --time-per-color 2 # liquidctl set logo color blinking 8000ff --time-per-color 2 --time-off 1 ``` The coolers support two more lighting modes: 'fixed' and 'blackout'. The latter is the only one to completely turn off the LED; however, it also inhibits the visual high-temperature alert. ``` # liquidctl set logo color fixed 00ff00 # liquidctl set logo color blackout ``` It is possible to configure the visual alert for high liquid temperatures: `--alert-threshold `: set the threshold temperature in Celsius for a visual alert `--alert-color `: set the color used by the visual high temperature alert Note that, regardless of the use of these options, alerts are always enabled (unless suppressed by the 'blackout' mode): the default threshold and color are, respectively, 45°C and red. liquidctl-1.5.1/docs/corsair-commander-guide.md000066400000000000000000000144341401367561700215020ustar00rootroot00000000000000# Corsair Commander Pro _Driver API and source code available in [`liquidctl.driver.commander_pro`](../liquidctl/driver/commander_pro.py)._ This driver will also work for the Corsair Lighting Node Pro Devices. ## Initializing the device The device should be initialized every time it is powered on, including when the system resumes from suspending to memory. The initialization command is needed in order to detect what temperature sensors and fan types are currently connected. ``` # liquidctl initialize Corsair Commander Pro (experimental) ├── Firmware version 0.9.212 ├── Bootloader version 0.5 ├── Temp sensor 1 Connected ├── Temp sensor 2 Connected ├── Temp sensor 3 Connected ├── Temp sensor 4 Connected ├── Fan 1 Mode DC ├── Fan 2 Mode DC ├── Fan 3 Mode DC ├── Fan 4 Mode Auto/Disconnected ├── Fan 5 Mode Auto/Disconnected └── Fan 6 Mode Auto/Disconnected ``` ``` # liquidctl initialize Corsair Lighting Node Pro (experimental) ├── Firmware version 0.10.4 └── Bootloader version 3.0 ``` ## Retrieving the fan speeds, temperatures and voltages The Lighting Node Pro does not have a status message. The Commander Pro is able to retrieve the current fan speeds as well as the current temperature of any connected temperature probes. Additionally the Commander Pro is able to retrieve the voltages from the 3.3, 5, and 12 volt buses. If a fan or temperature probe is not connected then a value of 0 is shown. ``` # liquidctl status Corsair Commander Pro (experimental) ├── 12 volt rail 12.06 V ├── 5 volt rail 4.96 V ├── 3.3 volt rail 3.36 V ├── Temp sensor 1 26.4 °C ├── Temp sensor 2 27.5 °C ├── Temp sensor 3 21.7 °C ├── Temp sensor 4 25.3 °C ├── Fan 1 speed 927 rpm ├── Fan 2 speed 927 rpm ├── Fan 3 speed 1195 rpm ├── Fan 4 speed 0 rpm ├── Fan 5 speed 0 rpm └── Fan 6 speed 0 rpm ``` ## Programming the fan speeds The Lighting Node Pro Does not have any fans to control. Each fan can be set to either a fixed duty cycle, or a profile consisting of up to six (temperature, rpm) pairs. Temperatures should be given in Celsius and rpm values as a valid rpm for the fan that is connected. *NOTE: you must ensure that the rpm value is within the min, max range for your hardware.* Profiles run on the device and are always based one the specified temp probe. If a temperature probe is not specified number 1 is used. The last point should set the fan to 100% fan speed, or be omitted; in the latter case the fan will be set to 5000 rpm at 60°C (this speed may not be appropriate for your device). ``` # liquidctl set fan1 speed 70 ^^^^ ^^ channel duty # liquidctl set fan2 speed 20 800 40 900 50 1000 60 1500 ^^^^^ ^^^^^ ^^^^^^ pairs of temperature (°C) -> duty (%) # liquidctl set fan3 speed 20 800 40 900 50 1300 --temp-probe 2 ``` Valid channel values are `fanN`, where 1 <= N <= 6 is the fan number, and `sync`, to simultaneously configure all fans. Only fans that have been connected and identified by `liquidctl initialize` can be set. Behaviour is unspecified if the specified temperature probe is not connected. _Note: pass `--verbose` to see the raw settings being sent to the cooler._ After normalization of the profile and enforcement of the (60°C, 5000) fail-safe. This temperature failsafe can be over-ridden by using the `--unsafe=high_temperature` flag. This will use a maximum temperature of 100 degrees. ## Controlling the LEDs The devices have 2 lighting channels that can have up to 96 leds connected to each. LED channels are specified as either `led1` or `led2` with channel 1 being the default. The table bellow summarizes the available modes, and their associated maximum number of colors. Note that for any effect if no colors are specified then random colors will be used. | Mode | Num colors | | ------------- | ---------- | | `clear` _¹_ | 0 | | `off` _²_ | 0 | | `fixed` | 1 | | `color_shift` | 2 | | `color_pulse` | 2 | | `color_wave` | 2 | | `visor` | 2 | | `blink` | 2 | | `marquee` | 1 | | `sequential` | 1 | | `rainbow` | 0 | | `rainbow2` | 0 | _¹: This is not a real mode but it will remove all saved effects_ _²: This is not a real mode but it is fixed with RGB values of 0_ To specify which LED's on the channel the effect should apply to the `--start-led` and `--maximum-leds` flags must be given. If you have 3 Corsair LL fans connected to channel one and you want to set the first and third to green and the middle to blue you can use the following commands: ``` # liquidctl set led1 color fixed 00ff00 --start-led 1 --maximum-leds 48 # liquidctl set led1 color fixed 0000ff --start-led 16 --maximum-leds 16 ``` This will first set all 48 leds to green then will set leds 16-32 to blue. Alternatively you could do: ``` # liquidctl set led1 color fixed 00ff00 --start-led 1 --maximum-leds 16 # liquidctl set led1 color fixed 0000ff --start-led 16 --maximum-leds 16 # liquidctl set led1 color fixed 00ff00 --start-led 32 --maximum-leds 16 ``` This allows you to compose more complex led effects then just the base modes. The different commands need to be sent in order that they should be applied. In the first example if the order were reversed then all of the LED's would be green. All of the effects support specifying a `--direction=forward` or `--direction=backward`. There are also 3 speeds that can be specified for the `--speed` flag. `fast`, `medium`, and `slow`. Each color can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). Currently the device can only accept hardware effects, and the specified configuration will persist across power offs. The changes take a couple of seconds to take effect. liquidctl-1.5.1/docs/corsair-hxi-rmi-psu-guide.md000066400000000000000000000042141401367561700217120ustar00rootroot00000000000000# Corsair HXi and RMi series PSUs _Driver API and source code available in [`liquidctl.driver.corsair_hid_psu`](../liquidctl/driver/corsair_hid_psu.py)._ ## Initialization It is necessary to initialize the device once it has been powered on. ``` # liquidctl initialize ``` The +12V rails normally functions in multiple-rail mode, and `initialize` will by default reset the PSU to that behavior. Single-rail mode can be optionally selected by passing `--single-12v-ocp` to `initialize`. _Note: changing the +12V OCP mode is currently an experimental feature._ ## Monitoring The PSU is able to report monitoring data about its own hardware and basic electrical variables for the input and output sides. ``` # liquidctl status Corsair RM650i ├── Current uptime 3:43:54 ├── Total uptime 9 days, 11:43:54 ├── Temperature 1 50.0 °C ├── Temperature 2 40.8 °C ├── Fan control mode Hardware ├── Fan speed 0 rpm ├── Input voltage 230.00 V ├── Total power 110.00 W ├── +12V OCP mode Multi rail ├── +12V output voltage 12.12 V ├── +12V output current 7.75 A ├── +12V output power 92.00 W ├── +5V output voltage 4.97 V ├── +5V output current 2.88 A ├── +5V output power 14.00 W ├── +3.3V output voltage 3.33 V ├── +3.3V output current 1.56 A └── +3.3V output power 5.00 W ``` ## Fan speed The fan speed is normally controlled automatically by the PSU. It is possible to override this and set the fan to a fixed duty value using the `fan` channel. ``` # liquidctl set fan speed 90 ``` This changes the fan control mode to software control and sets the minimum allowed duty value to 30%. To revert back to hardware control, re-`initialize` the device. liquidctl-1.5.1/docs/corsair-platinum-pro-xt-guide.md000066400000000000000000000104401401367561700226060ustar00rootroot00000000000000# Corsair Hydro Platinum and Pro XT all-in-one liquid coolers _Driver API and source code available in [`liquidctl.driver.hydro_platinum`](../liquidctl/driver/hydro_platinum.py)._ ## Initializing the device and setting the pump mode The device should be initialized every time it is powered on, including when the system resumes from suspending to memory. ``` # liquidctl initialize Corsair H100i Platinum (experimental) └── Firmware version 1.1.15 ``` By default the pump mode will be set to `balanced`, but a different mode can be specified with `--pump-mode`. The valid values for this option are `quiet`, `balanced` and `extreme`. ``` # liquidctl initialize --pump-mode extreme Corsair H100i Platinum (experimental) └── Firmware version 1.1.15 ``` Unconfigured fan channels may default to 100% duty, so [reprogramming their behavior](#programming-the-fan-speeds) is also recommended after running `initialize` for the first time since the cooler was powered on. Subsequent executions of `initialize` should leave the fan speeds unaffected. ## Retrieving the liquid temperature and fan/pump speeds The cooler reports the liquid temperature and the speeds of all fans and pump. ``` # liquidctl status Corsair H100i Platinum (experimental) ├── Liquid temperature 27.0 °C ├── Fan 1 speed 1386 rpm ├── Fan 2 speed 1389 rpm └── Pump speed 2357 rpm ``` ## Programming the fan speeds Each fan can be set to either a fixed duty cycle, or a profile consisting of up to seven (temperature, duty) pairs. Temperatures should be given in Celsius and duty values in percentage. Profiles run on the device and are always based on the internal liquid temperature probe. The last point should set the fan to 100% duty cycle, or be omitted; in the latter case the fan will be set to max out at 60°C. ``` # liquidctl set fan1 speed 70 ^^^^ ^^ channel duty # liquidctl set fan2 speed 20 20 40 70 50 100 ^^^^^ ^^^^^ ^^^^^^ pairs of temperature (°C) -> duty (%) ``` Valid channel values are `fanN`, where N >= 1 is the fan number, and `fan`, to simultaneously configure all fans. As mentioned before, unconfigured fan channels may default to 100% duty. _Note: pass `--verbose` to see the raw settings being sent to the cooler, after normalization of the profile and enforcement of the (60°C, 100%) fail-safe._ ## Controlling the LEDs In reality these coolers do not have the concept of different channels or modes, but liquidctl provides a few for convenience. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | LEDs | Components | Platinum | Pro XT | Platinum SE | | -------- | ----------- | ------------ | ------------ | -------- | ------ | ----------- | | led | off | synchronized | all off | 0 | 0 | 0 | | led | fixed | synchronized | independent | 1 | 1 | 1 | | led | super-fixed | independent | independent | 24 | 16 | 48 | The `led` channel can be used to address individual LEDs, and supports the `super-fixed`, `fixed` and `off` modes. In `super-fixed` mode, each color supplied on the command line is applied to one individual LED, successively. LEDs for which no color has been specified default to off/solid black. This is closest to how the device works. In `fixed` mode, all LEDs are set to a single color supplied on the command line. The `off` mode is simply an alias for `fixed 000000`. ``` # liquidctl set led color off # liquidctl set led color fixed ff8000 # liquidctl set led color fixed "hsv(90,85,70)" # liquidctl set led color super-fixed ^^^ ^^^^^^^^^^^ ^ channel mode colors... ``` Each color can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). Animations are not supported at the hardware level, and require successive invocations of the commands shown above, or use of the liquidctl APIs. Note: lighting control of Pro XT devices is experimental and requires the `--unsafe pro_xt_lighting` flag to be supplied on the command line. liquidctl-1.5.1/docs/ddr4-guide.md000066400000000000000000000114611401367561700167270ustar00rootroot00000000000000# DDR4 DIMMs _Driver API and source code available in [`liquidctl.driver.ddr4`](../liquidctl/driver/ddr4.py)._ Support for these DIMMs in only available on Linux. Other requirements must also be met: - `i2c-dev` kernel module has been loaded - r/w permissions to the host SMBus `/dev/i2c-*` device - specific unsafe features have been opted in - the host SMBus is supported: currently only i801 (Intel mainstream & HEDT) Jump to a specific section: - [DIMMs with a standard temperature sensor][ddr4_temperature] - [Corsair Vengeance RGB][vengeance_rgb] - *[Inherent unsafeness of I²C/SMBus]* ## DIMMs with a standard temperature sensor [ddr4_temperature]: #dimms-with-a-standard-temperature-sensor Supports modules using TSE2004-compatible SPDD EEPROMs with temperature sensor. Experimental. Unsafe features: - `smbus`: see [Inherent unsafeness of I²C/SMBus] - `ddr4_temperature`: access standard temperature sensor address ### Initialization Not required for this device. ### Retrieving the DIMM's temperature ``` # liquidctl status --unsafe=smbus,ddr4_temperature DDR4 DIMM2 (experimental) └── Temperature 30.5 °C ``` ## Corsair Vengeance RGB [vengeance_rgb]: #corsair-vengeance-rgb Experimental. Unsafe features: - `smbus`: see [Inherent unsafeness of I²C/SMBus] - `vengeance_rgb`: access non-advertised temperature sensor and RGB controller addresses ### Initialization Not required for this device. ### Retrieving the DIMM's temperature ``` # liquidctl status --verbose --unsafe=smbus,vengeance_rgb Corsair Vengeance RGB DIMM2 (experimental) └── Temperature 30.5 °C ``` ### Controlling the LED Each module features a few *non-addressable* RGB LEDs. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Colors | | ---------- | ----------- | -----: | | `led` | `off` | 0 | | `led` | `fixed` | 1 | | `led` | `breathing` | 1–7 | | `led` | `fading` | 2–7 | The LED colors can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). The speed of the breathing and fading animations can be adjusted with `--speed`; the allowed values are `slowest`, `slower`, `normal` (default), `faster` and `fastest`. ``` # liquidctl set led color breathing ff355e 1ab385 speed=faster --unsafe=smbus,vengeance_rgb ^^^ ^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ channel mode colors speed enable unsafe features # liquidctl set led color fading "hsv(90,85,70)" "hsv(162,85,70)" --unsafe=smbus,vengeance_rgb # liquidctl set led color fixed ff355e --unsafe=smbus,vengeance_rgb # liquidctl set led color off --unsafe=smbus,vengeance_rgb ``` ## Inherent unsafeness of I2C and SMBus [Inherent unsafeness of I²C/SMBus]: #inherent-unsafeness-of-i2c-and-smbus Reading and writing to System Management (SMBus) and I²C buses is inherently more risky than dealing with, for example, USB devices. On typical desktop and workstation systems many important chips are connected to these buses, and they may not tolerate writes or reads they do not expect. While SMBus 2.0 has some limited ability for automatic enumeration of devices connected to it, unlike simpler I²C buses and SMBus 1.0, this capability is, effectively, not safely available for us in user space. It is thus necessary to rely on certain devices being know to use a specific address, or being documented/specified to do so; but there is always some risk that another, unexpected, device is using that same address. The enumeration capability of SMBus 2.0 also brings dynamic address assignment, so even if a device is know to use a particular address in one machine, that could be different on other systems. On top of this, accessing I²C or SMBus buses concurrently, from multiple threads or processes, may also result in undesirable or unpredictable behavior. Unsurprisingly, users or programs dealing with I²C/SMBus devices have occasionally crashed systems and even bricked boards or peripherals. In some cases this is reversible, but not always. For all of these reasons liquidctl requires users to *opt into* accessing I²C/SMBus devices, which can be done by enabling the `smbus` unsafe feature. Other unsafe features may also be required for the use of specific devices, based on other *know* risks specific to a particular device. Note that a feature not being labeled unsafe, or a device not requiring the use of additional unsafe features, does in no way assure that it is safe. This is especially true when dealing with I²C/SMBus devices. Finally, liquidctl may list some I²C/SMBus devices even if `smbus` has not been enabled, but only if it is able to discover them without communicating with the bus or the devices. liquidctl-1.5.1/docs/developer/000077500000000000000000000000001401367561700164375ustar00rootroot00000000000000liquidctl-1.5.1/docs/developer/capturing-usb-traffic.md000066400000000000000000000114421401367561700231620ustar00rootroot00000000000000# Capturing USB traffic ## Preface A fundamental aspect of developing drivers for USB devices is inspecting the traffic between applications and the device. This is useful for debugging your own drivers and applications, as well as to understand undocumented protocols. In the latter case, a possibly opaque and closed source application is allowed to communicate with the device, and the captured traffic is analyzed to understand what the device is capable of and what it expects from the host application. ## Capturing USB traffic on a native Windows host Get [Wireshark]. During the Wireshark setup, enable the installation of USBPcap for experimental capturing of USB traffic. Reboot. To capture some USB traffic, start Wireshark, double click the USBPcap1 interface to start capturing all traffic on it, and proceed to [Finding the target device](#finding-the-target-device). _If you have more than one USBPcap interface, you may need to look for the target devices in each of them._ ## Capturing USB traffic on Linux _and capturing USB traffic in a Windows VM, through the Linux host_ You will need to install [Wireshark] from your favorite package manager. You may have to run Wireshark as root to be able to capture USB traffic. Alternatively you can give the your normal user permissions to capture traffic by adding your self to the `wireshark` group and granting yourself read permissions on the `/dev/usbmon*` devices. Some extra steps may be needed, you can follow the instructions [here](https://wiki.wireshark.org/CaptureSetup/USB). Note you may need to logout and login agin for these changes to take effect. The general steps are as follows: There are a number of different virtualization tools you can use, such as [virt-manager](https://virt-manager.org/) a front end for [kvm/qemu](https://www.qemu.org/), [virtualbox](https://www.virtualbox.org/), and many more. You can google how to do the specific tasks for the one you wish to use. 1. Create a windows VM using your favourite virtual machine manager 2. Install the software to control the device 3. Start Wireshark to capture the traffic 3. Pass the usb device through to the virtual machine 4. Start changing settings using the app and watch the messages appear in the Wireshark interface. 5. Success! ## Finding the target device [Finding the target device](#finding-the-target-device) Wireshark captures USB traffic at the bus level, which means that all devices on that bus will be captured. This is a lot of noise, so the first step is find the target device among all others and filter the traffic to that device. _For this example, assume the target device has vendor and product IDs `0x1e71` and `0x170e`, respectively._ First, filter (top bar) the `GET DESCRIPTOR` response for this device: ``` usb.idVendor == 0x1e71 && usb.idProduct == 0x170e ``` Next, select the filtered packet and, on the middle panel, expand the USB URB details, right click "Device address" and select Apply as Filter -> Selected. This should result in a new filter that resembles: ``` usb.device_address == 2 ``` And only packets to or from that device should be displayed. ## Exporting captured data There are two main useful ways to work with Wireshark captures of USB traffic. The first is within Wireshark itself, using its native PCAP format (or any of its variants), which is useful for manual analysis. _PCAP files are also the preferred way of storing and sharing captured USB traffic._ You can simply File -> Save to export captured traffic from Wireshark. But for more control over what will be exported (for example, only currently filtered/displayed packets), File -> Export Specified Packets is generally preferred. The other way of analyzing USB traffic is through external, and sometimes custom, tools. In theses cases it may be helpful to additionally export the data to JSON (File -> Export Packet Dissections -> As JSON). Plain text or CSV dissections are _not_ very useful with USB data, since Wireshark tends to truncate the long fields that are of our interest. ## Next steps Once you have your usb capture (probably in a `pcapng` format). You may find it easer to look at the data outside of Wireshark since Wireshark sometimes limits the number of bytes shown to fewer then the amount you want to look at. I generally like to use `tshark`, one of the cli tools that comes with Wireshark to extract the only the fields I care about so that I can easily only show the fields I can about and can use other bash commands to separate the fields and remove some of the extraneous messages (for example Corsair iCue sends a get status message every second which I am generally not interested in). Next steps would be to take a look at (analyzing USB protocols)[techniques-for-analyzing-usb-protocols.md] [Wireshark]: https://www.wireshark.org [USBPcap]: https://desowin.org/usbpcap/ liquidctl-1.5.1/docs/developer/porting-drivers-from-opencorsairlink.md000066400000000000000000000216471401367561700262720ustar00rootroot00000000000000# Porting drivers from OpenCorsairLink _Originally posted as a [comment in issue #129](https://github.com/liquidctl/liquidctl/issues/129#issuecomment-640258429)._ In essence, writing a new liquidctl driver means implementing all (suitable) methods of a [`liquidctl.base.BaseDriver`](https://github.com/liquidctl/liquidctl/blob/master/liquidctl/driver/base.py#L9). Note that you shouldn't _directly_ subclass the BaseDriver; instead you'll inherit from a bus-specific base driver like `liquidctl.usb.UsbDriver` or `liquidctl.usb.UsbHidDevice`, which will already include default implementations for many methods and properties. And for the new driver to work out-of-the-box it's sufficient to import its module in [`liquidctl/driver/__init__.py`](https://github.com/liquidctl/liquidctl/blob/master/liquidctl/driver/__init__.py#L23). Next, in order to port a driver from OCL, the first step is to check the `corsair_device_info` struct that matches the device, which defines the low-level and driver (protocol) functions used for it in OCL, besides a few other important parameters. ```c { .vendor_id = 0x1b1c, .product_id = 0x0c04, .device_id = 0x3b, .name = "H80i", .read_endpoint = 0x01 | LIBUSB_ENDPOINT_IN, .write_endpoint = 0x00 | LIBUSB_ENDPOINT_OUT, .driver = &corsairlink_driver_coolit, .lowlevel = &corsairlink_lowlevel_coolit, .led_control_count = 1, .fan_control_count = 4, .pump_index = 5, }, ``` —_[in `device.c`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/device.c#L107-L119)_ ## The low-level functions Starting with the low-level functions specified by [`corsairlink_lowlevel_coolit`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/drivers/coolit.c#L27-L32), and implemented in [`lowlevel/coolit.c`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/lowlevel/coolit.c): the equivalence between these and the methods in a liquidctl driver is: - `init` -> `connect` (in some cases and/or `initialize`) - `deinit` -> `disconnect` - `read`/`write` -> `self.device.read`/`self.device.write` (see next paragraphs) This is a HID device, so the liquidctl driver should inherit [`liquidctl.usb.UsbHidDriver`](https://github.com/liquidctl/liquidctl/blob/c9f2244200a552ce8af3d64b937d3b01cebdb126/liquidctl/driver/usb.py), meaning that in the driver `self.device` will be a `liquidctl.usb.HidapiDevice`. Additionally, liquidctl already automatically handles how to write to a HID, but does so mimicking hidapi; `HidapiDevice.write` follows the specification: > The first byte of data[] must contain the Report ID. For devices which only support a single report, this must be set to 0x0. The remaining bytes contain the report data. Since the Report ID is mandatory, calls to hid_write() will always contain one more byte than the report contains. >—_from [`hidapi/hidapi.h`](https://github.com/libusb/hidapi/blob/24a822c80f95ae1b46a7a3c16008858dc4d8aec8/hidapi/hidapi.h#L185-L213)_ Practically, it means that you only need to implement `init` and `deinit`, and that in the translated driver, when OCL would call `corsairlink_coolit_write` with `[byte1, byte2, byte3, ...]`, you'll instead call `self.device.write` with `[0x00, byte1, byte2, byte3, ...]` (note the prepended 0x00 byte) ## Higher-level functionality The remaining `get_status`, `set_fixed_speed`, `set_speed_profile` and `set_color` methods (required by BaseDriver) will encapsulate the functionality specified by [`corsairlink_driver_coolit`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/drivers/coolit.c#L34) (implemented in `protocol/coolit/*.c`), and are for the most part what users will access through the CLI. Data that is read from the cooler, like the pump speed, will generally go into `get_status`. The firmware version is an exception in this case: it's read with a specific command (instead of being part of other replies), and so it belongs in the output of `initialize`. _(You can fetch the firmware version directly in `initialize` or, if you need to use it anywhere else, you read it and cache it in `connect`, and only return the cached value in `initialize`.)_ The other three methods are self-explanatory and should be fairly straightforward to implement, apart from the special considerations that I go into next. ## Protocols with _interdependent_ messages A big aspect in the design of the liquidctl CLI was not requiring the user to configure different aspects of the cooler in a single command: you should be able to set the pump speed without resetting the fan speed or the LED colors. For most devices there's a clear mapping between the CLI and the implementation: the CLI command `set speed ` implemented with `set_fixed_speed` won't depend on other BaseDriver methods (apart from `connect` and `disconnect`). There are however "complicated" devices where, at the protocol level, functionality is grouped (all channels must be set at once) or even completely consolidated into a single "state" (everything must be reset when changing a single parameter). Messages can also be required to follow an arbitrary order. So, besides looking at how each individual parameter is configured, you also need to check the "logic" part of OCL, in this case implemented in [`hydro_coolit_settings`](https://github.com/audiohacked/OpenCorsairLink/blob/testing/logic/settings/hydro_coolit.c#L32). This doesn't mean that all OCL devices will fall into the "complicated" category, or that you'll necessarily need to match that order exactly. In fact, in the case of the H80i (or other devices using the same protocol) I think that the different aspects of the cooler can indeed be configured independently, at least for the most part. This is mostly due to the empty implementations of `init` and `deinit`: in more complex cases these functions usually involve some type of opening and closing of a "transaction", but there's nothing of the sort here. The ordering in `hydro_coolit_settings` also seems to be strictly due to natural requirements (you need to know how many sensors there are before reading them), instead of being totally arbitrary. But I could be wrong... Anyway, the main concern I have right now is the [`CommandId`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/include/protocol/coolit.h#L93) byte that's sent in every message. It starts at 0x81 and is continually incremented. On one hand it clearly doesn't need to be a perfect sequence number (as OCL doesn't guarantee that in multiple invocations), but on the other the shorter message chains in liquidctl (due to only a few parameters being read or changed at a time) could cause the cooler to complain. I'd start following OCL: initialize a similar variable to 0x81 every time the driver is instantiated, and increment it every time it's used. But if that somehow doesn't work, you can use the internal [`keyval`](https://github.com/liquidctl/liquidctl/blob/master/liquidctl/keyval.py#L1) API ([example usage](https://github.com/liquidctl/liquidctl/blob/4e649bead665bf692d7df9b8bc1a9a79791d356d/liquidctl/driver/asetek.py#L281)) to temporarily persist it to disk, allowing you to implement a true (wrapping) sequence number _across_ liquidctl invocations. No matter what, just don't forget to explicitly wrap `CommandId` it at 255, you'll probably be using a normal Python integer instead of a `u8`. ## Advanced driver binding liquidctl driver don't normally need to check anything super special to know whether or not they are compatible with a particular device. As long as `SUPPORTED_DEVICES` lists the compatible USB vendor and product IDs, besides any additional parameters required by `__init__`, the bus-specific base driver will do the rest. This wont be the case with the H80i: it shares a common vendor and product ID with other devices, and is only differentiated by a "device ID", that has to be explicitly read. Reading of this device ID is implemented in OCL by [`corsairlink_coolit_device_id`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/protocol/coolit/core.c#L32). There are two ways of handling this in liquidctl. One way is to override `probe` (implemented in `UsbHidDriver`) to fetch the device ID, filter out any unknown IDs, and (only) yield driver instances that have as field a know ID; each instance should also map that ID to the corresponding parameters for that device (`description`, fan count, pump index, etc.). Another way is to have a generic driver that only fetches the ID and customizes itself accordingly at `connect` time, meaning that before that it identifies itself as something like "Undetermined Corsair device". Because having the driver instance in an undetermined state will cause some issues, both for us and for the user, I think you should try the `probe` method first. liquidctl-1.5.1/docs/developer/protocol/000077500000000000000000000000001401367561700203005ustar00rootroot00000000000000liquidctl-1.5.1/docs/developer/protocol/lighting_node_rgb.md000066400000000000000000000117061401367561700242730ustar00rootroot00000000000000# Corsair Commander Pro and Lighting Node Pro protocol ### Compatible devices | Device Name | USB ID | LED channels | Fan channels | | ----------- | ------ | ------------ | ------------ | | Commander Pro | `1B1C:0C10` | 2 | 6 | | Lighting Node Pro | `1B1C:0C0B` | 2 | 0 | ### Command formats Host -> Device: 16 bytes Device -> Host: 64 bytes ## Get Information commands ### Get Firmware version - `0x02` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | X | | 0x02 | Y | | 0x03 | Z | Firmware version is `X.Y.Z` ### Get Bootloader version - `0x06` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | X | | 0x02 | Y | Bootloader version is `X.Y` ### Get temperature sensor configuration - `0x10` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | `0x01` temp sensor 1 connected, otherwise `0x00` | | 0x02 | `0x01` temp sensor 2 connected, otherwise `0x00` | | 0x03 | `0x01` temp sensor 3 connected, otherwise `0x00` | | 0x04 | `0x01` temp sensor 4 connected, otherwise `0x00` | ### Get temperature value - `0x11` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x11 | | 0x01 | temp sensor number (0x00 - 0x03) | Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | temp MSB | | 0x02 | temp LSB | Divide the temperature value by 100 to get the value is degrees celsius. ### Get bus voltage value - `0x12` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x12 | | 0x01 | rail number (0x00 - 0x02) | rail 0 = 12v rail 1 = 5v rail 2 = 3.3v Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | voltage MSB | | 0x02 | voltage LSB | Divide the value by 1000 to get the actual voltage. ### Get fan configuration - `0x20` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | Fan 1 mode | | 0x02 | Fan 2 mode | | 0x03 | Fan 3 mode | | 0x04 | Fan 4 mode | | 0x05 | Fan 5 mode | | 0x06 | Fan 6 mode | Fan modes: | Fan Mode | Value | | ------------ | ----- | | Disconnected | 0x00 | | DC (3 pin) | 0x01 | | PWM (4 pin) | 0x02 | ### Get fan RPM value - `0x21` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x21 | | 0x01 | fan number (0x00 - 0x05) | Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | rpm MSB | | 0x02 | rpm LSB | ## Set commands ### Set fixed % - `0x23` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x23 | | 0x01 | fan number (0x00 - 0x05) | | 0x02 | percentage | ### Set fan curve % - `0x25` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x25 | | 0x01 | fan number (0x00 - 0x05) | | 0x02 | temp sensor to use (0x00 - 0x03) 0xFF to use external sensor | | 0x03, 0x04 | temp point 1 MSB | | 0x05, 0x06 | temp point 2 MSB | | 0x07, 0x08 | temp point 3 MSB | | 0x09, 0x0A | temp point 4 MSB | | 0x0B, 0x0C | temp point 5 MSB | | 0x0D, 0x0E | temp point 6 MSB | | 0x0F, 0x10 | rpm point 1 MSB | | 0x11, 0x12 | rpm point 2 MSB | | 0x13, 0x14 | rpm point 3 MSB | | 0x15, 0x16 | rpm point 4 MSB | | 0x17, 0x18 | rpm point 5 MSB | | 0x19, 0x1A | rpm point 6 MSB | ### Hardware LED commands - Send reset channel - send start LED effect - set channel to hardware mode - send effect (one or more messages) - send commit ### Reset channel - `0x37` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x37 | | 0x01 | channel number (0x00 or 0x01) | ### Start channel LED effect - `0x34` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x34 | | 0x01 | channel number (0x00 or 0x01) | ### Set channel state - `0x38` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x38 | | 0x01 | channel number (0x00 or 0x01) | | 0x02 | 0x01 hardware control, 0x02 software control | ### Set LED effect - `0x35` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x35 | | 0x01 | channel number (0x00 or 0x01) | | 0x02 | start LED number | | 0x03 | number of LEDs | | 0x04 | mode | | 0x05 | speed | | 0x06 | direction | | 0x07 | random colors | | 0x08 | 0xFF | | mode | value | Num Colors | | ---- | ----- | --------- | | rainbow | `0x00` | 0 | | color_shift | `0x01` | 2 | | color_pulse | `0x02` | 2 | | color_wave | `0x03` | 2 | | fixed | `0x04` | 1 | | visor | `0x06` | 2 | | marquee | `0x07` | 1 | | blink | `0x08` | 2 | | sequential | `0x09` | 1 | | rainbow2 | `0x0A` | 0 | | speed | value | | ----- | ----- | | slow | `0x02` | | medium | `0x01` | | fast | `0x00` | | direction | value | | ----- | ----- | | forward | `0x01` | | backward | `0x00` | ### Commit hardware settings - `0x33` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x33 | | 0x01 | 0xFF | liquidctl-1.5.1/docs/developer/protocol/vengeance_rgb.md000066400000000000000000000067031401367561700234150ustar00rootroot00000000000000# Corsair Vengeance RGB DDR4 UDIMMs Unbuffered DDR4 modules with a 4 Kbit SPD EEPROM, a temperature sensor and non-addressable RGB LEDs. The SPD EEPROM does *not* advertise the presence of the temperature sensor. These are I²C devices, connected to the host's SMBus. Each memory module module exposes three I²C devices, using a 4-bit Device Type Identifier Code (DTIC) and a 3-bit Select Address (SA) to generate each I²C Bus Slave Address. The Select Address is set by the host through the dedicated SA0, SA1 and SA2 pins on the DDR4 288-pin connector, and is shared by all I²C devices on the module. Because CorsairLink and iCue acquire kernel locks on the relevant I²C devices, capturing the traffic from/to those devices with software tools is severely limited. Fortunately, connecting a logic analyzer to the SCL and SDA pins of a memory slot (directly or through the use of some dummy module) is a cheap and effective alternative. ## Device 0x50 | SA: SPD EEPROM EE1004-compatible SPD EEPROM. See [JEDEC 21-C 4.1.6] and [JEDEC 21-C 4.1.2.L-5]. ## Device 0x18 | SA: temperature sensor Appears to support the same registers and features as in a TSE10004 SPD EEPROM with temperature sensor, *except for I²C block reads.* Instead, the registers should be read as words, with the caveat that the temperature sensor register values are supposed to be read in big endianess (MSB first, then LSB), but SMBus Read Word Data assumes the data is returned in little endianess (LSB first, then MSB). For the register map, see [JEDEC 21-C 4.1.6]. Note: the SPD EEPROM does not advertise the presence of the temperature sensor. ## Device 0x58 | SA: RGB controller Register map: | Register | Size in bytes | Purpose | | :-- | :-: | :-- | | `0xa4` | 1 | Timing parameter 1 | | `0xa5` | 1 | Timing parameter 2 | | `0xa6` | 1 | Lighting mode | | `0xa7` | 1 | Number of colors | | `0xb0–0xc4` | 1 | Red, green and blue components (up to 7 colors) | Lighting modes: | Value | Animation | | :-- | :-- | | `0x00` | Static or breathing with a *single* color | | `0x01` | Fading with 2–7 colors | | `0x02` | Breathing with 2–7 colors | Timing parameter 1 (TP1): influences the total time in the transition states: increasing *and* decreasing brightness, or fading through from one color to the next. The valid range for animations appears to be from 1 to at least 63, but a consistent conversion to seconds cannot be inferred; the special value 0 disables the animation. Timing parameter 2 (TP2): influences the total time in the stable minimum *and* maximum brightness states. The valid range appears to be from 0 to at least 63, but a consistent conversion to seconds cannot be inferred. Note: CorsairLink always sets both T2 and T1 equally. ## References ([JEDEC 21-C 4.1.6]) Definitions of the EE1004-v 4 Kbit Serial Presence Detect (SPD) EEPROM and TSE2004av 4 Kbit SPD EEPROM with Temperature Sensor (TS) for Memory Module Applications. [JEDEC 21-C 4.1.6]: https://www.jedec.org/standards-documents/docs/spd416 ([JEDEC 21-C 4.1.2.L-5]) Annex L: Serial Presence Detect (SPD) for DDR4 SDRAM Modules. [JEDEC 21-C 4.1.2.L-5]: https://www.jedec.org/standards-documents/docs/spd412l-5 [SMBus captures] in liquidctl/collected-device-data. [SMBus captures]: https://github.com/liquidctl/collected-device-data/tree/master/Corsair%20Vengeance%20RGB [OpenRGB wiki entry] for Corsair Vengeance RGB modules. [OpenRGB wiki entry]: https://gitlab.com/CalcProgrammer1/OpenRGB/-/wikis/Corsair-Vengeance-RGB liquidctl-1.5.1/docs/developer/release-checklist.md000066400000000000000000000044741401367561700223610ustar00rootroot00000000000000# Release checklist ## Prepare system - [ ] Install publishing dependencies: twine ## Prepare repository - [ ] Update liquidctl/version.py - [ ] Update last update data in the man page - [ ] Make sure the CHANGELOG is up to date - [ ] Update the link in the README to the stable executable for Windows - [ ] Remove "U/Starting with upcoming..." notes from the table of supported devices - [ ] Commit "release: prepare for v" ## Test With `"$(liquidctl --version) = "liquidclt v"`: - [ ] Run unit and doc tests: `pytest` - [ ] Run my setup scripts: `liquidcfg && liquiddyncfg` - [ ] Run old HW tests: `extra/old-tests/asetek_*` and `extra/old-tests/kraken_two` - [ ] Test krakenduty: `extra/krakenduty-poc train && extra/krakenduty-poc status` - [ ] Test krakencurve: `extra/krakencurve-poc control --fan-sensor coretemp.package_id_0 --pump '(25,50),(35,100)' --fan '(25,35),(35,60),(60,100)' --verbose` - [ ] Test yoda: `extra/yoda --match kraken control pump with '(20,50),(50,100)' on coretemp.package_id_0 and fan with '(20,25),(34,100)' on _internal.liquid --verbose` - [ ] Test liquiddump: `extra/liquiddump | jq -c .` - [ ] Test krakenx (git): `colctl --mode fading --color_count 2 --color0 192,32,64 --color1 246,11,21 --fan_speed "(30, 100), (40, 100)" --pump_speed "(30, 100), (40, 100)"` ## Source distribution - [ ] Tag HEAD with `git tag -as v` and short summary annotation (signed) - [ ] Push HEAD and `v` tag - [ ] Check all CI statuses (pytest, flake8 linting, and `list --verbose`) - [ ] Generate the source distribution: `python setup.py sdist` - [ ] Check that all necessary files are in `dist/liquidctl-.tar.gz` and that generated `extraversion.py` makes sense - [ ] Sign the source distribution: `gpg --detach-sign -a dist/liquidctl-.tar.gz` ## Binary distribution for Windows - [ ] Download and check artifact built by AppVeyor - [ ] Sign the artifact: `gpg --detach-sign -a dist/liquidctl--bin-windows-x86_64.zip` ## Release - [ ] Upload: `twine upload dist/liquidctl-.tar.gz{,.asc}` - [ ] Upgrade the `v` tag on GitHub to a release (with sdist, Windows artifact, and corresponding GPG signatures) - [ ] Update the HEAD changelog with the release file SHA256 sums ## Post release - [ ] Update ArchLinux `liquidctl-git` liquidctl-1.5.1/docs/developer/style-guide.md000066400000000000000000000103701401367561700212150ustar00rootroot00000000000000# Style guide This is not the code; this is just a tribute. ## General guidelines This section has yet to be written, but for a start... Read [PEP 8], then immediately watch Raymond Hettinger's [Beyond PEP 8] talk. Write code somewhere between those lines. In this repository, newer drivers are usually better examples than older ones. Experience with the domain helps to write better code. Try to keep lines around 80-ish columns wide; in some modules 100-ish columns may be more suited, but definitively try to avoid going beyond that. Be consistent within a given module; *try* to be consistent between similar modules. [PEP 8]: https://pep8.org/ [Beyond PEP 8]: https://www.youtube.com/watch?v=wf-BqAjZb8M ### Nits - indent with 4 spaces - prefer to continue lines vertically, aligned with the opening delimiter - prefer single quotes for string literals, unless double quotes will avoid some escaping - use f-strings whenever applicable, except for `logging` messages (more about those bellow) which wont necessarily be shown - use lowercase hexadecimal literals ## Use of automatic formatters Pull requests are welcome. _(For a suggestion of a formatter and associated configuration to use, not just to fill this section)._ ## Writing messages Liquidctl generates a substantial amount of human readable messages, either intended for direct consumption by CLI users, or for _a posteriori_ debugging by developers. While discussing the format of these messages deviates from the usual tab × spaces wars we have learned to love [citation needed], it is very important to keep the messages clear, objective and consistent. ### Errors Raise exceptions, preferably `liquidctl.error.*` or Python's [built-in exceptions]. Error messages should start with a lowercase letter. [built-in exceptions]: https://docs.python.org/3/library/exceptions.html ### Warnings [warnings]: #warnings Warnings are used to convey information that are deemed always relevant. They should be used to alert of unexpected conditions, degraded states, and other high-importance issues. By default the CLI outputs these to `stderr` for all users. Since the messages are visible to non-developers, the should follow the simplified guidelines bellow: - start with a lowercase letter - string values that the user is expected to type, or that absolutely need to delimited: use single quotes - other values: no quotes or backticks - command line commands or options: no quotes or backticks Deprecated guidelines for warnings: - outputting values as `key=value` pairs ### Info Information messages are normally hidden, but can be enabled with the `--verbose` or `--debug` flags. They should be used to display complementary information that may interest any user, like additional `status` items or computed `temp × rpm` curves. Since these messages are also targeted to non-developers, they should follow the same simplified guidelines used for [warnings]. ### Debug Debug messages are only intended for developers or users interested in dealing with the internals of the program. In the CLI these are normally disabled and require the `--debug` flag. Since they are intended for internal use, they follow relaxed guidelines: - start with a lowercase letter - use `key=value`, `` `expression` ``, `'string'`, or `value`, whichever is clearer ### A quick refresher on `logging` Get a logger at the start of the module. ```py import logging ... _LOGGER = logging.getLogger(__name__) ``` Prefer old-style %-formatting for logging messages, since this is evaluated lazyly by `logging`, and the message will only be formated if the logging level is enabled. ```py _LOGGER.warning('value %d, expected %d', current, expected) ``` _(While `%d` and `%i` are equivalent, and both print integers, prefer the former over the latter)._ _(The rest of the time `f-strings` are preferred, following the `PEP 498` guideline)._ When writing informational or debug messages, pay attention to the cost of computing each value. A classic example is hex formatting some bytes, which can be expensive; this case can be solved by using `liquidctl.util.LazyHexRepr`, and other similar wrapper types can be constructed for other scenarios. ```py from liquidctl.util import LazyHexRepr ... _LOGGER.debug('buffer: %r', LazyHexRepr(some_bytes)) ``` liquidctl-1.5.1/docs/developer/techniques-for-analyzing-usb-protocols.md000066400000000000000000000277421401367561700265340ustar00rootroot00000000000000# Techniques for analyzing USB protocols _Originally posted as a [comment in issue #142](https://github.com/liquidctl/liquidctl/issues/142#issuecomment-650568291)._ ## USB transfers At a basic level you can view USB traffic as a collection of transfers. Transfers can be of a few different types, but for the most part we're only interested in control transfers, interrupt transfers and, very occasionally, bulk transfers. I'll skip over the purposes of each type since we'll merely use whichever ones the protocol mandates. But our devices at not necessarily always manipulated as USB devices. In fact, it's rather common that we need to work on a layer further up the abstraction chain, on a Human Interface Device (HID). USB HIDs are a special type of USB device specified by a corresponding "interface class".¹ Working with HID protocols is almost identical to working with other classes of USB devices: they use control transfers and interrupt transfers, and we capture them with Wireshark. The distinction does matter in a few places, most notability around the "report ID", but I'll get to that later... ## Wireshark, part one So, you have captured some USB traffic to your device with Wireshark. How do you approach that data? First, I would filter the packets to only those coming from or being sent to the device you're interested at. If you know the device address in the bus you can filter with `usb.device_address ==
`, and in Linux it's easy to find the device address with `lsusb`. I usually save these results into a new file, and only use it from that point forward. Another way to find the device address, which can be useful when dealing with old captures or captures made in another OS or machine, it to look in the various `GET DESCRIPTOR DEVICE` responses for the one that matches the `idVendor` you're interested in. You should know that the address on the bus can change, from OS to OS, from boot to boot or if the device is reconnected. Next, I would add a few custom columns: `usb.data_fragment` for data sent in control transfers, and `usb.capdata` for data exchanged in the other types of transfers. _Update: the latest versions of Wireshark have improved HID decoding capabilities, and HID data may also appear in `usbhid.data`._ Wireshark actually works one level of abstraction bellow what I called a transfer, with USB request blocks (URBs), so there's a lot of uninteresting entries in the captured data. You can reduce this by ignoring URBs without any data_fragment or capdata, since only in a few cases these are useful in understanding the protocol. Most of the protocol we need to implement lies in these two Wireshark fields. In interrupt or bulk transfers all data is this `capdata`, the rest is just USB metadata. Control transfers do need to be inspected more carefully, but their use outside of HIDs is very rare (Asetek 690LC coolers being one example). With HIDs is common to see control transfers, particularly if the device has no OUT endpoint (an endpoint is something you write to xor read from). In this case then all writes will be sent as control transfers (instead of interrupt transfers), usually as `SET_REPORT` requests; and this is also where the report numbers I mentioned before become important. HIDs don't just send raw or opaque packets of bytes. They have the concept of a report, which is supposed to structure the data and make the device capabilities self describing. A HID can support a single unnumbered report, or one or more numbered reports. Knowing the correct report ID (or its absence) is part of understanding the protocol, and is especially important when `SET_REPORT` transfers are involved. ![wireshark-hid-set-report2](https://user-images.githubusercontent.com/1832496/85924521-3fd19c80-b869-11ea-9bd1-43f5db6fe6ce.png) The report ID can be decoded from `wValue` argument of the transfer: the most significant byte (MSB) is the report type (0x01 for input, 0x02 for output, 0x3 for feature) and the LSB is the report ID, or zero if the device doesn't use report IDs. Both values are import when implementing the protocol. Some protocols also read data from HIDs with a `GET_REPORT` request, instead of directly from the incoming endpoint. In those cases the report type and ID will also be in `wValue`. ## Groups of transfers Decoding the protocol involves figuring out, for each action of interest, which transfers are involved, what parameters are sent in each transfer, and how they are encoded. The devices we're working with have two very separate sets of actions: - reading data (usually fan/pump/temperature monitoring, but sometimes also less variable device information such as firmware version or accessories) - writing new device configuration (fan or pump speeds, lighting animations and colors, etc.) Sometimes reading monitoring data requires a previous write or the use of an explicit `GET_REPORT` request. Besides obvious `GET_REPORT` requests, you may spot other protocol-specific write transfers that appear to serve no purpose other than to request data. Other times reads will simply timeout if not preceded by a write, which you'll only discover with some experimentation. Mapping between actions and transfers is usually simple, based on what fields you can identify in the data of each transfer. For example, the presence of color parameters is almost always very easy to spot, and clearly indicates some type of color-related configuration. ## Common fields and field encodings ### Fan and/or pump speed (read) Usually in u16le or u16be (16-bit unsigned integer of either endianess). In the case of power suplies, could also be encoded in LINEAR11/LINEAR16, as defined by the PMBus specification ([`liquidctl.pmbus.linear_to_float`]). [`liquidctl.pmbus.linear_to_float`]: https://github.com/liquidctl/liquidctl/blob/d1b8d2424948c564e218e2f0cf5ffb86f21b1445/liquidctl/pmbus.py#L104 ### Fan and/or pump duty values (read/write) Usually a single byte, either as a fraction of 100 (0–100) or 255 (0–255). ### Temperature (read) Usually some custom type of fixed point decimal, taking two bytes. One of the bytes is almost always `floor(temperature)`; the other is used for the remainder, encoded as a fraction of 10 (0–10) or 255 (0–255). "Endianess" varies. ### Temperature (write) When temperatures are sent to the device, either as part of a speed profile or to trigger visual alerts, they are almost always simple single-byte integer values (context will dictate whether should use round, floor or ceil). ### LED colors (write) Almost universally sent as 24-bit RGB. However, endianess varies, and some devices may also use custom orderings. In summary, any order of 16-bit red, green and blue values for each color. ### CRC checksums (read/write) Some devices end all messages (received and sent) with a 8-bit checksum (also known as a PEC byte). They usually follow the SMBus specification and use the `x⁸ + x² + x¹ + x⁰` polynomial ([`liquidctl.pmbus.compute_pec(bytes)`]). [`liquidctl.pmbus.compute_pec(bytes)`]: https://github.com/liquidctl/liquidctl/blob/d1b8d2424948c564e218e2f0cf5ffb86f21b1445/liquidctl/pmbus.py#L168 ### Action type (read/write) This indicates to the cooloer how it should interpret and act on the rest of the message. Usually a "command" byte, but may also be the report ID. ### Sequence numbers (read/write) Sometimes received and sent on every transfer. May also be (shifted and) OR'ed with a "command" byte or other indicator. You can spot this in byte offsets where the value follows a pattern that repeats every n transfers. May or may not be required for correct operation of the device. ## Techniques for identifying fields Spotting fields in transfers is critical. They tell you not only where to place things, but also what each message is used for. The first technique is simply to watch the transfers and compare, in real time, with values shown/entered in the software you're using to interact with the cooler. This works well if the protocol is simple, and if the rate of transfers is small. In other scenarios it doesn't work that well, but may still be required to decode specific parts of the protocol. A related technique, particurlary when you think you're already close to understanding the protocol, is to try to read and/or write some packets yourself (make sure your writes at least resemble the real packets before trying this; sending arbitrary data to the device is a bad idea). Next you can start to analyze the data in batches, that is, looking at a set of transactions at once. In many cases some fields will become immediately obvious this way: you can see that there's a field, and the value will tell you what it's related to. Taking things one step further, you can compute some basic statistics for every byte offset: min, max, average, median, .... Even if there's a lot of noise (either from too many messages, too many unknown fields, or both) this will usually make the interesting fields more visible. If you see a byte more or less uniformely distributed in the 0–255 range, it's likely a LSB of a two-byte field; if you see bytes with some variance but that are restricted to a specific range, just try to decode them as a fraction of 10 or 255; finally, the second byte of a two-byte field is usually (but not always) just before or just after the first. A similar idea can be applied to bitfields: you count how many times the bit in each position changes, and this will usually tell you where the LSBs are (they are the ones that change the most). This technique could also be applied to the entire transfer, but this isn't really necessary in the protocols we're dealing with. While doing all/any of the above, you still want to pay some attention to the parts of the message you don't understand. They can be "random noise" (either in the true sense or not), but there may be important things in there as well. If you spot patterns, particularly those _not_ of a simple constant, it's worth trying to make sense of what that value could represent. It could be a required aspect of the protocol, or an additional monitoring variable not shown in the official GUI (for example: voltage and current for a fan channel). In the end, it comes down to spotting patterns, and besides knowing more or less what you're looking for and how it's usually encoded, you get better with experience. But it's not very hard, it may just take more than a couple of tries in the really tricky cases. ## Wireshark, part two, and other tools _this section is incomplete; there are many tools, and I'm not particularly good with any of them_ You should know you can export the Wireshark capture to JSON. In fact, it's the only way I know of of getting `usb.capdata` and `usb.data_fragment` off of Wireshark into a standard data manipulation format _without truncation._ This can be done from within the Wireshark UI, or with `tshark`. tshark -r .pcapng -T json > .json You can preprocess this data with `jq`, and then further manipulate it in any tool you're familiar with. Heck, sometimes I even use spreadsheets (not very elegant, I know). You also easily write a custom script to do some analyses or test hypothesis on these JSON captures. For an example, check the [script I used when working the Platinum coolers]. [script I used when working the Platinum coolers]: https://github.com/liquidctl/collected-device-data/blob/master/Corsair%20H115i%20RGB%20Platinum/analyze.py Alternatively you can use: tshark -r .pcapng -2 -e "frame.number" -e "usb.data_fragment" -e "usb.capdata" -Tfields > dump.txt To pull out only the frame number, data_fragment, and capdata fields and output them in to a txt file. The frame number field is really usefull for if you have a seperate text description file that had a description of what commands got set to the device and arround what frame number the message corresponds to. (you can write down the number of one of the bottom messages shown in the wireshark console while the command is being sent) ## Notes _¹ There are Bluetooth HIDs, but these obviously aren't very relevant here._ liquidctl-1.5.1/docs/gigabyte-rgb-fusion2-guide.md000066400000000000000000000066221401367561700220230ustar00rootroot00000000000000# Gigabyte RGB Fusion 2.0 lighting controllers _Driver API and source code available in [`liquidctl.driver.rgb_fusion2`](../liquidctl/driver/rgb_fusion2.py)._ RGB Fusion 2.0 is a lighting system that supports 12V non-addressable RGB and 5V addressable ARGB lighting accessories, alongside RGB/ARGB memory modules and other elements on the motherboard itself. It is built into motherboards that contain the RGB Fusion 2.0 logo, typically from Gigabyte. These motherboards use one of many possible ITE Tech controller chips, which are connected to the host via SMBus or USB, depending on the motherboard/chip model. A couple of USB controllers are currently supported: - ITE 5702: found in Gigabyte Z490 Vision D - ITE 8297: found in Gigabyte X570 Aorus Elite ## Initialization RGB Fusion 2.0 controllers must be initialized after the system boots or resumes from a suspended state. ``` # liquidctl initialize Gigabyte RGB Fusion 2.0 5702 Controller ├── Hardware name IT5702-GIGABYTE V1.0.10.0 └── Firmware version 1.0.10.0 ``` ## Lighting The controllers support six color modes: `off`, `fixed`, `pulse`, `flash`, `double-flash` and `color-cycle`. As much as we prefer to use descriptive channel names, currently it is not practical to do so, since the correspondence between the hardware channels and the corresponding features on the motherboard is not stable. Hence, lighting channels are given generic names: `led1`, `led2`, etc.; at this time, eight are defined. In addition to these, it is also possible to use the `sync` pseudo-channel to apply a setting to all lighting channels. ``` # liquidctl set sync color off # liquidctl set led1 color fixed 350017 # liquidctl set led2 color pulse ff2608 # liquidctl set led3 color flash 350017 # liquidctl set led4 color double-flash 350017 # liquidctl set led5 color color-cycle --speed slower ``` For color modes `pulse`, `flash`, `double-flash` and `color-cycle`, the animation speed is governed by the optional `--speed` parameter, with one of six possible values: `slowest`, `slower`, `normal` (the default), `faster`, `fastest` or `ludicrous`. The more elaborate color/animation schemes supported by the motherboard on the addressable headers are not currently supported. ## Correspondence between lighting channels and physical locations Each user may need to create a table that associates generic channel names to specific areas or headers on their motherboard. For example, a map for the Gigabyte Z490 Vision D might look like: - led1: the LED next to the IO panel; - led2: one of two 12V RGB headers; - led3: the LED on the PCH chip ("Designare" on Vision D); - led4: an array of LEDs behind the PCI slots on *back side* of motherboard; - led5: second 12V RGB header; - led6: one of two 5V addressable RGB headers; - led7: second 5V addressable RGB header; - led8: not in use. ## More on resuming from sleep states On wake-from-sleep, the ITE controller will be reset and all color modes will revert to fixed blue. To work around this, the methods used to [automate the configuration at boot time] should be adapted to also handle resuming from sleep states. On macOS it is also possible to use the _sleepwatcher_ utility, installed via Homebrew, along with a script to run on wake that issues the necessary liquidctl commands and restores desired lighting effects. [automate the configuration at boot time]: ../README.md#automation-and-running-at-boot liquidctl-1.5.1/docs/kraken-x2-m2-guide.md000066400000000000000000000135711401367561700202140ustar00rootroot00000000000000# Third-generation NZXT liquid coolers _Driver API and source code available in [`liquidctl.driver.kraken2`](../liquidctl/driver/kraken2.py)._ ## NZXT Kraken X42, X52, X62, X72 The Kraken X42, X52, X62 and X72 are the third generation of Kraken X liquid coolers by NZXT. These devices are manufactured by Asetek and house fifth generation Asetek pumps, plus secondary PCBs specially designed by NZXT for enhanced control and lighting. They incorporate customizable fan and pump speed control with PWM, a liquid temperature probe in the block and addressable RGB lighting. The coolers are powered directly from the power supply unit. All configuration is done through USB, and persists as long as the device still gets power, even if the system has gone to Soft Off (S5) state. The cooler also reports fan and pump speed and liquid temperature via USB; pump speed can also be sent to the motherboard (or other device) via the sense pin of a standard fan connector. All capabilities available at the hardware level are supported, but other features offered by CAM, like presets based on CPU or GPU temperatures, have not been implemented. Pump and fan control based on liquid temperature is supported on units running firmware versions 4 or above. ## NZXT Kraken M22 This driver also supports the NZXT Kraken M22. However, this device has no pump or fan control, nor reports liquid temperatures. ## Monitoring The cooler can report the fan and pump speed, as well as the liquid temperature. ``` # liquidctl status NZXT Kraken X (X42, X52, X62 or X72) ├── Liquid temperature 29.9 °C ├── Fan speed 853 rpm ├── Pump speed 1948 rpm └── Firmware version 6.0.2 ``` ## Fan and pump speeds First, some important notes... *You must carefully consider what pump and fan speeds to run. Heat output, case airflow, radiator size, installed fans and ambient temperature are some of the factors to take into account. Test your settings under different scenarios, and make sure that they are appropriate, correctly applied and persistent.* *Additionally, the liquid temperature should never reach 60°C, as at that point the pump and tubes might fail or quickly degrade. You must monitor this during your tests and make any necessary adjustments. As a safety measure, fan and pump speeds will forcibly be programmed to 100% for liquid temperatures of 60°C and above.* *You should also consider monitoring your hardware temperatures and setting alerts for overheating components or pump failures.* With those out of the way, each channel can be independently configured to a fixed duty value or with a profile dependent on the liquid temperature. Fixed speeds can be set by specifying the desired channel – `fan` or `pump` – and duty. ``` # liquidctl set pump speed 90 ``` | Channel | Minimum duty | Maximum duty | | --- | --- | --- | | fan | 25% | 100% | | pump | 50% | 100% | *Another important note: pump speeds between 50% and 60% are not currently exposed in CAM. Presumably, there might be some scenarios when these lower speeds are not suitable.* For profiles, one or more temperature–duty pairs must be supplied. liquidctl will normalize and optimize this profile before pushing it to the Kraken. Adding `--verbose` will trace the final profile that is being applied. ``` # liquidctl set fan speed 20 30 30 50 34 80 40 90 50 100 ``` ## RGB lighting For lighting, the user can control a total of nine LEDs: one behind the NZXT logo and eight forming the ring that surrounds it. These are separated into two channels, independently accessed through `logo` and `ring`, or synchronized with `sync`. ``` # liquidctl set sync color fixed af5a2f # liquidctl set ring color fading 350017 ff2608 # liquidctl set logo color pulse ffffff # liquidctl set ring color marquee-5 2f6017 --direction backward --speed slower ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports different number of colors. The animation speed can be customized with the `--speed `, and five relative values are accepted by the device: `slowest`, `slower`, `normal`, `faster` and `fastest`. Some of the color animations can be in either the `forward` or `backward` direction. This can be specified by using the `--direction` flag. | `ring` | `logo` | `sync` | Mode | Colors | Notes | | --- | --- | --- | --- | --- | --- | | ✓ | ✓ | ✓ | `off` | None | | ✓ | ✓ | ✓ | `fixed` | One | | ✓ | ✓ | ✓ | `super-fixed` | Up to 9 (logo + each ring LED) | | ✓ | ✓ | ✓ | `fading` | Between 2 and 8, one for each step | | ✓ | ✓ | ✓ | `spectrum-wave` | None | | ✓ | | | `super-wave` | Up to 8 | | ✓ | | | `marquee-` | One | 3 ≤ `length` ≤ 6 | | ✓ | | | `covering-marquee` | Up to 8, one for each step | | ✓ | | | `alternating` | Two | | ✓ | | | `moving-alternating` | Two | | ✓ | ✓ | ✓ | `breathing` | Up to 8, one for each step | | ✓ | ✓ | ✓ | `super-breathing` | Up to 9 (logo + each ring LED) | Only one step | | ✓ | ✓ | ✓ | `pulse` | Up to 8, one for each pulse | | ✓ | | | `tai-chi` | Two | | ✓ | | | `water-cooler` | None | | ✓ | | | `loading` | One | | ✓ | | | `wings` | One | #### Deprecated modes The following modes are now deprecated and the use of the `--direction backward` is preferred, they will be removed in a future version and are kept for now for backwards compatibility. | `ring` | `logo` | `sync` | Mode | Colors | Notes | | --- | --- | --- | --- | --- | --- | | ✓ | ✓ | ✓ | `backwards-spectrum-wave` | None | | ✓ | | | `backwards-super-wave` | Up to 8 | | ✓ | | | `backwards-marquee-` | One | 3 ≤ `length` ≤ 6 | | ✓ | | | `covering-backwards-marquee` | Up to 8, one for each step | | ✓ | | | `backwards-moving-alternating` | Two | liquidctl-1.5.1/docs/kraken-x3-z3-guide.md000066400000000000000000000163431401367561700202330ustar00rootroot00000000000000# Fourth-generation NZXT liquid coolers _Driver API and source code available in [`liquidctl.driver.kraken3`](../liquidctl/driver/kraken3.py)._ The fourth-generation of NZXT Kraken coolers is composed by X models—featuring the familiar infinity mirror—and Z models—replacing the infinity mirror with an OLED screen. Both X and Z models house seventh-generation Asetek pump designs, plus secondary PCBs from NZXT for enhanced control and visual customization. The coolers are powered directly from the power supply unit. All configuration is done through USB, and persists as long as the device still gets power, even if the system has gone to Soft Off (S5) state. The coolers also report relevant data via USB, including pump and/or fan speeds and liquid temperature. The pump speed can be sent to the motherboard (or other device) via the sense pin of a standard fan connector. ## NZXT Kraken X53, X63, X73 The X models incorporate customizable pump speed control, a liquid temperature probe in the block and addressable RGB lighting. In comparison with the previous generation of X42/X52/X62/X72 coolers, fan control is no longer provided. All capabilities available at the hardware level are supported, but other features offered by CAM, like presets based on CPU or GPU temperatures, are not part of the scope of the liquidctl CLI. ## NZXT Kraken Z63, Z73 The most notable difference between Kraken X and Kraken Z models is the replacement of the infinity mirror by a OLED screen. In addition to this, Kraken Z coolers restore the embedded fan controller that is missing from the current Kraken X models. The OLED screen cannot yet be controlled with liquidctl, but all other hardware capabilities are supported. ## Initialization Devices must be initialized being read or written to. This is necessary after powering on from Mechanical Off, or if there has been hardware changes. Only then monitoring, proper fan control and all lighting effects will be available. The firmware version and all connected LED accessories are reported during the device initialization. ``` # liquidctl initialize NZXT Kraken X (X53, X63 or X73) ├── Firmware version 1.8.0 ├── LED accessory 1 HUE 2 LED Strip 300 mm ├── LED accessory 1 AER RGB 2 140 mm ├── LED accessory 2 AER RGB 2 140 mm ├── Pump Logo LEDs detected └── Pump Ring LEDs detected ``` ## Monitoring The cooler can report the pump speed and liquid temperature. ``` # liquidctl status NZXT Kraken X (X53, X63 or X73) ├── Liquid temperature 24.1 °C ├── Pump speed 1869 rpm └── Pump duty 60 % ``` ## Fan and pump speeds First, some important notes... *You must carefully consider what pump and fan speeds to run. Heat output, case airflow, radiator size, installed fans and ambient temperature are some of the factors to take into account. Test your settings under different scenarios, and make sure that they are appropriate, correctly applied and persistent.* *The X models do not provide a way to control your fan speeds. You must set those fan curves wherever you plugged your fans in (e.g. motherboard).* *Additionally, the liquid temperature should never reach 60°C, as at that point the pump and tubes might fail or quickly degrade. You must monitor this during your tests and make any necessary adjustments. As a safety measure, pump speed will forcibly be programmed to 100% for liquid temperatures of 60°C and above.* *You should also consider monitoring your hardware temperatures and setting alerts for overheating components or pump failures.* With those out of the way, the pump speed can be configured to a fixed duty value or with a profile dependent on the liquid temperature. Fixed speeds can be set by specifying the desired channel and duty value. ``` # liquidctl set pump speed 90 ``` | Channel | Minimum duty | Maximum duty | X models | Z models | | --- | --- | --- | :---: | :---: | | `pump` | 20% | 100% | ✓ | ✓ | | `fan` | 20% | 100% | | ✓ | For profiles, one or more temperature–duty pairs are supplied instead of single value. ``` # liquidctl set pump speed 20 30 30 50 34 80 40 90 50 100 ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^^ pairs of temperature (°C) -> duty (%) ``` liquidctl will normalize and optimize this profile before pushing it to the Kraken. Adding `--verbose` will trace the final profile that is being applied. ## RGB lighting with LEDs One or more LED channels are provided, depending on the model. | Channel | Type | LED count | X models | Z models | | --- | --- | --- | :---: | :---: | | `external` | HUE 2/HUE+ accessories | up to 40 | ✓ | ✓ | | `ring` | Infinity mirror: ring | 8 | ✓ | | | `logo` | Infinity mirror: logo | 1 | ✓ | | | `sync` | Synchronize all channels | up to 40 | ✓ | | Color modes can be set independently for each lighting channel, but the specified color mode will then apply to all devices daisy chained on that channel. ``` # liquidctl set sync color fixed af5a2f # liquidctl set ring color fading 350017 ff2608 # liquidctl set logo color pulse ffffff # liquidctl set external color marquee-5 2f6017 --direction backward --speed slower ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports different number of colors. The animation speed can be customized with the `--speed `, and five relative values are accepted by the device: `slowest`, `slower`, `normal`, `faster` and `fastest`. Some of the color animations can be in either the `forward` or `backward` direction. This can be specified by using the `--direction` flag. | Mode | Colors | Variable speed | | --- | --- | :---: | | `off` | None | | | `fixed` | One | | | `fading` | Between 1 and 8 | ✓ | | | `super-fixed` | Between 1 and 40 | | | `spectrum-wave` | None | ✓ | | `marquee-`, 3 ≤ length ≤ 6 | One | ✓ | | `covering-marquee` | Between 1 and 8 | ✓ | | `alternating-` | Between 1 and 2 | ✓ | | `moving-alternating-`, 3 ≤ length ≤ 6 | Between 1 and 2 | ✓ | | `pulse` | Between 1 and 8 | ✓ | | `breathing` | Between 1 and 8 | ✓ | | `super-breathing` | Between 1 and 40 | ✓ | | `candle` | One | | | `starry-night` | One | ✓ | | `rainbow-flow` | None | ✓ | | `super-rainbow` | None | ✓ | | `rainbow-pulse` | None | ✓ | | `loading` | One | | | `tai-chi` | Between 1 and 2 | ✓ | | `water-cooler` | Two | ✓ | | `wings` | One | ✓ | #### Deprecated modes The following modes are now deprecated and the use of the `--direction backward` is preferred, they will be removed in a future version and are kept for now for backwards compatibility. | Mode | Colors | Variable speed | | --- | --- | :---: | | `backwards-spectrum-wave` | None | ✓ | | `backwards-marquee-`, 3 ≤ length ≤ 6 | One | ✓ | | `covering-backwards-marquee` | Between 1 and 8 | ✓ | | `backwards-moving-alternating-`, 3 ≤ length ≤ 6 | Between 1 and 2 | ✓ | | `backwards-rainbow-flow` | None | ✓ | | `backwards-super-rainbow` | None | ✓ | | `backwards-rainbow-pulse` | None | ✓ | ## The OLED screen (only Z models) To be implemented. liquidctl-1.5.1/docs/linux/000077500000000000000000000000001401367561700156115ustar00rootroot00000000000000liquidctl-1.5.1/docs/linux/making-systemd-units-wait-for-devices.md000066400000000000000000000060421401367561700253770ustar00rootroot00000000000000# Making systemd units wait for devices When using a systemd service to configure devices at boot time, as suggested in [Automation and running at boot/Set up Linux using system](../../README.md#set-up-linux-using-systemd), it can sometimes happen that the hardware is not ready when the service tries to start. A blunt solution to this is to add a small delay, but a more robust alternative is to make the service unit depend on the corresponding hardware being available at the OS level. ## Systemd device units For this it is first necessary to set up systemd to create device units with known names. This is done with udev rules, specifically with `TAG+="systemd"` (to create a device unit) and a memorable `SYMLINK+=""` name. ``` # /etc/udev/rules.d/99-liquidctl-custom.rules # Example udev rules to create device units for some specific liquidctl devices. # create a dev-kraken.device for this third-generation Kraken X ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="170e", ATTRS{serial}=="", SYMLINK+="kraken", TAG+="systemd" # create a dev-clc120.device for this EVGA CLC ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="2433", ATTRS{idProduct}=="b200", SYMLINK+="clc120", TAG+="systemd" ``` Setting a custom name with `SYMLINK` is optional: just with `TAG+="systemd"` alone a device unit will be made available as `dev-bus-usb--.device`, where the `` and the `` numbers can be found with the `lsusb` command. ## Setting the dependencies The new device units can then be added as dependencies to the service unit. ``` # /etc/systemd/system/liquidcfg.service [Unit] Description=AIO startup service Requires=dev-kraken.device Requires=dev-clc120.device After=dev-kraken.device After=dev-clc120.device ... ``` With these changes in place, and after rebooting the system, the service should begin to wait for the devices before trying to starting. Notes: - the `SUBSYSTEM` value must match how liquidctl connects to the device; devices listed by liquidctl as on a `hid` bus should use the value `hidraw`, while the remaining should use `usb` - when possible it is good to include the serial number in the match, to account for the possibility of multiple units of the same model - on the service unit file `Requires=` is used instead of `Wants=` because we want a [strong dependency](https://www.freedesktop.org/software/systemd/man/systemd.unit.html#%5BUnit%5D%20Section%20Options) - rebooting the system is not technically necessary, but triggering the new udev rules without a reboot is outside the scope of this document - some devices may still not be able to response just after being discovered by udev, in which case a delay is really necessary ## Alternative approach An alternative approach is to have systemd start the configuration service when the device is found by udev, by making the device depend on the service: ``` ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="170e", ATTRS{serial}=="" ENV{SYSTEMD_WANTS}="liquidcfg.service" ``` liquidctl-1.5.1/docs/nvidia-guide.md000066400000000000000000000145311401367561700173450ustar00rootroot00000000000000# NVIDIA graphics cards _Driver API and source code available in [`liquidctl.driver.nvidia`](../liquidctl/driver/nvidia.py)._ Support for these cards in only available on Linux. Other requirements must also be met: - `i2c-dev` kernel module has been loaded - r/w permissions to card-specific `/dev/i2c-*` devices - specific unsafe features have been opted in Jump to a specific section: * _Series 10/Pascal:_ - [EVGA GTX 1080 FTW](#evga-gtx-1080-ftw) * _Series 20/Turing:_ - [ASUS Strix RTX 2080 Ti OC](#asus-strix-rtx-2080-ti-oc) * _[Inherent unsafeness of I²C/SMBus]_ ## EVGA GTX 1080 FTW Only RGB lighting supported. Unsafe features: - `smbus`: see [Inherent unsafeness of I²C/SMBus] ### Initialization Not required for this device. ### Retrieving the current RGB lighting mode and color In verbose mode `status` reports the current RGB lighting settings. ``` # liquidctl status --verbose --unsafe=smbus EVGA GTX 1080 FTW ├── Mode Fixed └── Color 2aff00 ``` ### Controlling the LED This GPU only has one led that can be set. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Colors | | ---------- | ----------- | -----: | | `led` | `off` | 0 | | `led` | `fixed` | 1 | | `led` | `breathing` | 1 | | `led` | `rainbow` | 0 | ``` # liquidctl set led color off --unsafe=smbus # liquidctl set led color rainbow --unsafe=smbus # liquidctl set led color fixed ff8000 --unsafe=smbus # liquidctl set led color breathing "hsv(90,85,70)" --unsafe=smbus ^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ channel mode color unsafe features ``` The LED color can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `--non-volatile`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. ``` # liquidctl set led color fixed 00ff00 --non-volatile --unsafe=smbus ``` ## ASUS Strix RTX 2080 Ti OC Only RGB lighting supported. Unsafe features: - `smbus`: see [Inherent unsafeness of I²C/SMBus] ### Initialization Not required for this device. ### Retrieving the current color mode and LED color In verbose mode `status` reports the current RGB lighting settings. ``` # liquidctl status --verbose --unsafe=smbus ASUS Strix RTX 2080 Ti OC ├── Mode Fixed └── Color ff0000 ``` ### Controlling the LED This GPU only has one led that can be set. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | Colors | | ---------- | ------------- | -----: | | `led` | `off` | 0 | | `led` | `fixed` | 1 | | `led` | `flash` | 1 | | `led` | `breathing` | 1 | | `led` | `rainbow` | 0 | ``` # liquidctl set led color off --unsafe=smbus # liquidctl set led color rainbow --unsafe=smbus # liquidctl set led color fixed ff8000 --unsafe=smbus # liquidctl set led color flash ff8000 --unsafe=smbus # liquidctl set led color breathing "hsv(90,85,70)" --unsafe=smbus ^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ channel mode color unsafe features ``` The LED color can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `--non-volatile`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. ``` # liquidctl set led color fixed 00ff00 --non-volatile --unsafe=smbus ``` Note: The `off` mode is simply an alias for `fixed 000000`. ## Inherent unsafeness of I2C and SMBus [Inherent unsafeness of I²C/SMBus]: #inherent-unsafeness-of-i2c-and-smbus Reading and writing to System Management (SMBus) and I²C buses is inherently more risky than dealing with, for example, USB devices. On typical desktop and workstation systems many important chips are connected to these buses, and they may not tolerate writes or reads they do not expect. While SMBus 2.0 has some limited ability for automatic enumeration of devices connected to it, unlike simpler I²C buses and SMBus 1.0, this capability is, effectively, not safely available for us in user space. It is thus necessary to rely on certain devices being know to use a specific address, or being documented/specified to do so; but there is always some risk that another, unexpected, device is using that same address. The enumeration capability of SMBus 2.0 also brings dynamic address assignment, so even if a device is know to use a particular address in one machine, that could be different on other systems. On top of this, accessing I²C or SMBus buses concurrently, from multiple threads or processes, may also result in undesirable or unpredictable behavior. Unsurprisingly, users or programs dealing with I²C/SMBus devices have occasionally crashed systems and even bricked boards or peripherals. In some cases this is reversible, but not always. For all of these reasons liquidctl requires users to *opt into* accessing I²C/SMBus devices, which can be done by enabling the `smbus` unsafe feature. Other unsafe features may also be required for the use of specific devices, based on other *know* risks specific to a particular device. Note that a feature not being labeled unsafe, or a device not requiring the use of additional unsafe features, does in no way assure that it is safe. This is especially true when dealing with I²C/SMBus devices. Finally, liquidctl may list some I²C/SMBus devices even if `smbus` has not been enabled, but only if it is able to discover them without communicating with the bus or the devices. liquidctl-1.5.1/docs/nzxt-e-series-psu-guide.md000066400000000000000000000032421401367561700214120ustar00rootroot00000000000000# NZXT E-series PSUs _Driver API and source code available in [`liquidctl.driver.nzxt_epsu`](../liquidctl/driver/nzxt_epsu.py)._ ## Initialization It is necessary to initialize the device once it has been powered on. ``` # liquidctl initialize ``` _Note: at the moment initialize is a no-op, but this is likely to change once more features are added to the driver._ ## Monitoring The PSU is able to report monitoring data about its own hardware and the output rails. ``` # liquidctl status NZXT E500 (experimental) ├── Temperature 45.0 °C ├── Fan speed 505 rpm ├── Firmware version A017/40983 ├── +12V peripherals output voltage 11.89 V ├── +12V peripherals output current 7.75 A ├── +12V peripherals output power 14.48 W ├── +12V EPS/ATX12V output voltage 11.95 V ├── +12V EPS/ATX12V output current 0.00 A ├── +12V EPS/ATX12V output power 0.00 W ├── +12V motherboard/PCI-e output voltage 11.96 V ├── +12V motherboard/PCI-e output current 1.00 A ├── +12V motherboard/PCI-e output power 11.95 W ├── +5V combined output voltage 4.90 V ├── +5V combined output current 0.02 A ├── +5V combined output power 0.11 W ├── +3.3V combined output voltage 3.23 V ├── +3.3V combined output current 0.01 A └── +3.3V combined output power 0.02 W ``` liquidctl-1.5.1/docs/nzxt-hue2-guide.md000066400000000000000000000135221401367561700177360ustar00rootroot00000000000000# NZXT HUE 2 and Smart Device V2 controllers _Driver API and source code available in [`liquidctl.driver.smart_device`](../liquidctl/driver/smart_device.py)._ The NZXT HUE 2 lighting system is a refresh of the original HUE+. The main improvement is the ability to mix Aer RGB 2 fans and HUE 2 lighting accessories (e.g. HUE 2 LED strip, HUE 2 Underglow, HUE 2 Cable Comb) in a channel. HUE+ devices, including the original Aer RGB fans, are also supported, but HUE 2 components cannot be mixed with HUE+ components in the same channel. Each channel supports up to 6 accessories and a total of 40 LEDs, and the firmware exposes several color presets, most of them common to other NZXT products. All configuration is done through USB, and persists as long as the device still gets power, even if the system has gone to Soft Off (S5) state. Most capabilities available at the hardware level are supported, but other features offered by CAM, like noise level optimization and presets based on CPU/GPU temperatures, have not been implemented. ## NZXT HUE 2 The NZXT HUE 2 controller features four lighting channels. ## NZXT HUE 2 Ambient The NZXT HUE 2 Ambient controller features two lighting channels. ## NZXT Smart Device V2 The NZXT Smart Device V2 is a HUE 2 variant of the original Smart Device fan and LED controller, that ships with NZXT's cases released in mid-2019 including the H510 Elite, H510i, H710i, and H210i. It provides two HUE 2 lighting channels and three independent fan channels with standard 4-pin connectors. Both PWM and DC fan control is supported, and the device automatically chooses the appropriate mode for each channel; the device also reports the state of each fan channel, as well as speed and duty (from 0% to 100%). A microphone is still present onboard for noise level optimization through CAM and AI. ## NZXT RGB & Fan Controller The NZXT RGB & Fan Controller is a retail version of the NZXT Smart Device V2. ## Initialization After powering on from Mechanical Off, or if there have been hardware changes, the device must first be initialized. Only then monitoring, proper fan control and all lighting effects will be available. The firmware version and the connected LED accessories are also reported during device initialization. ``` # liquidctl initialize NZXT Smart Device V2 ├── Firmware version 1.5.0 ├── LED 1 accessory 1 HUE 2 LED Strip 300 mm ├── LED 1 accessory 2 HUE 2 Underglow 200 mm ├── LED 1 accessory 3 HUE 2 Underglow 200 mm ├── LED 2 accessory 1 AER RGB 2 140 mm ├── LED 2 accessory 2 AER RGB 2 140 mm ├── LED 2 accessory 3 AER RGB 2 140 mm └── LED 2 accessory 4 AER RGB 2 120 mm ``` ## Monitoring The device can report fan information for each channel and the noise level at the onboard sensor ``` # liquidctl status NZXT Smart Device V2 ├── Fan 2 duty 42 % ├── Fan 2 speed 934 rpm └── Noise level 62 dB ``` ## Fan speeds _Only NZXT Smart Device V2_ Fan speeds can only be set to fixed duty values. ``` # liquidctl set fan2 speed 90 ``` | Channel | Minimum duty | Maximum duty | Note | | --- | --- | --- | - | | fan1 | 0% | 100% || | fan2 | 0% | 100% || | fan3 | 0% | 100% || | sync | 0% | 100% | all available channels | *Always check that the settings are appropriate for the use case, and that they correctly apply and persist.* ## RGB lighting LED channels are numbered sequentially: `led1`, `led2`, (only HUE 2: `led3`, `led4`). Color modes can be set independently for each lighting channel, but the specified color mode will then apply to all devices daisy chained on that channel. There is also a `sync` channel. ``` # liquidctl set led1 color fixed af5a2f # liquidctl set led2 color fading 350017 ff2608 --speed slower # liquidctl set led3 color pulse ffffff # liquidctl set led4 color marquee-5 2f6017 --direction backward --speed slowest # liquidctl set sync color spectrum-wave ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports different number of colors. The animation speed can be customized with the `--speed `, and five relative values are accepted by the device: `slowest`, `slower`, `normal`, `faster` and `fastest`. Some of the color animations can be in either the `forward` or `backward` direction. This can be specified by using the `--direction` flag. | Mode | Colors | Notes | | --- | --- | --- | | `off` | None | | `fixed` | One | | `super-fixed` | Up to 40, one for each LED | | `fading` | Between 2 and 8, one for each step | | `spectrum-wave` | None | | `marquee-` | One | 3 ≤ `length` ≤ 6 | | `covering-marquee` | Up to 8, one for each step | | `alternating-` | Two | 3 ≤ `length` ≤ 6 | | `moving-alternating-` | Two | 3 ≤ `length` ≤ 6 | | `pulse` | Up to 8, one for each pulse | | `breathing` | Up to 8, one for each step | | `super-breathing` | Up to 40, one for each LED | Only one step | | `candle` | One | | `starry-night` | One | | `rainbow-flow` | None | | `super-rainbow` | None | | `rainbow-pulse` | None | | `wings` | One | #### Deprecated modes The following modes are now deprecated and the use of the `--direction backward` is preferred, they will be removed in a future version and are kept for now for backwards compatibility. | Mode | Colors | Notes | | --- | --- | --- | | `backwards-spectrum-wave` | None | | `backwards-marquee-` | One | 3 ≤ `length` ≤ 6 | | `covering-backwards-marquee` | Up to 8, one for each step | | `backwards-moving-alternating-` | Two | 3 ≤ `length` ≤ 6 | | `backwards-rainbow-flow` | None | | `backwards-super-rainbow` | None | | `backwards-rainbow-pulse` | None | liquidctl-1.5.1/docs/nzxt-smart-device-v1-guide.md000066400000000000000000000121041401367561700217750ustar00rootroot00000000000000# NZXT Smart Device (V1) and Grid+ V3 _Driver API and source code available in [`liquidctl.driver.smart_device`](../liquidctl/driver/smart_device.py)._ The Smart Device is a fan and LED controller that ships with the H200i, H400i, H500i and H700i cases. It provides three independent fan channels with standard 4-pin connectors. Both PWM and DC control is supported, and the device automatically chooses the appropriate mode. Additionally, up to four chained HUE+ LED strips or five chained Aer RGB fans can be driven from a single RGB channel. The firmware installed on the device exposes several presets, most of them familiar to other NZXT products. A microphone is also present onboard for noise level optimization through CAM and AI. This driver also supports the NZXT Grid+ V3 fan controller, which has six fan speed channels but no LED support or microphone. All configuration is done through USB, and persists as long as the device still gets power, even if the system has gone to Soft Off (S5) state. The device also reports the state of each fan channel, as well as speed, voltage and current. All capabilities available at the hardware level are supported, but other features offered by CAM, like noise level optimization and presets based on CPU/GPU temperatures, have not been implemented. ## Initialization After powering on from Mechanical Off, or if there have been hardware changes, the device must first be initialized. This takes a few seconds and should detect all connected fans and LED accessories. Only then monitoring, proper fan control and all lighting effects will be available. ``` # liquidctl initialize ``` ## Monitoring The device can report fan information for each channel, the noise level at the onboard sensor, as well as the type of the connected LED accessories. ``` # liquidctl status NZXT Smart Device (V1) ├── Fan 1 PWM ├── Fan 1 current 0.04 A ├── Fan 1 speed 1064 rpm ├── Fan 1 voltage 11.91 V ├── Fan 2 PWM ├── Fan 2 current 0.01 A ├── Fan 2 speed 1051 rpm ├── Fan 2 voltage 11.77 V ├── Fan 3 PWM ├── Fan 3 current 0.09 A ├── Fan 3 speed 1581 rpm ├── Fan 3 voltage 11.77 V ├── Firmware version 1.0.7 ├── LED accessories 2 ├── LED accessory type HUE+ Strip ├── LED count (total) 20 └── Noise level 62 dB ``` ## Fan speeds Fan speeds can only be set to fixed duty values. ``` # liquidctl set fan2 speed 90 ``` | Channel | Minimum duty | Maximum duty | Note | | --- | --- | --- | - | | fan1 | 0% | 100% || | fan2 | 0% | 100% || | fan3 | 0% | 100% || | fan4 | 0% | 100% | Grid+ V3 only | | fan5 | 0% | 100% | Grid+ V3 only | | fan6 | 0% | 100% | Grid+ V3 only | | sync | 0% | 100% | all available channels | *Always check that the settings are appropriate for the use case, and that they correctly apply and persist.* ## RGB lighting _Only NZXT Smart Device (V1)_ For lighting, the user can control up to 40 LEDs, if all four strips or five fans are connected. They are chained in a single channel: `led`. ``` # liquidctl set led color fixed af5a2f # liquidctl set led color fading 350017 ff2608 --speed slower # liquidctl set led color pulse ffffff # liquidctl set led color marquee-5 2f6017 --direction backward --speed slowest ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports different number of colors. The animation speed can be customized with the `--speed `, and five relative values are accepted by the device: `slowest`, `slower`, `normal`, `faster` and `fastest`. Some of the color animations can be in either the `forward` or `backward` direction. This can be specified by using the `--direction` flag. | Mode | Colors | Notes | | --- | --- | --- | | `off` | None | | `fixed` | One | | `super-fixed` | Up to 40, one for each LED | | `fading` | Between 2 and 8, one for each step | | `spectrum-wave` | None | | `super-wave` | Up to 40 | | `marquee-` | One | 3 ≤ `length` ≤ 6 | | `covering-marquee` | Up to 8, one for each step | | `alternating` | Two | | `moving-alternating` | Two | | `breathing` | Up to 8, one for each step | | `super-breathing` | Up to 40, one for each LED | Only one step | | `pulse` | Up to 8, one for each pulse | | `candle` | One | | `wings` | One | #### Deprecated modes The following modes are now deprecated and the use of the `--direction backward` is preferred, they will be removed in a future version and are kept for now for backwards compatibility. | Mode | Colors | Notes | | --- | --- | --- | | `backwards-spectrum-wave` | None | | `backwards-super-wave` | Up to 40 | | `backwards-marquee-` | One | 3 ≤ `length` ≤ 6 | | `covering-backwards-marquee` | Up to 8, one for each step | | `backwards-moving-alternating` | Two | liquidctl-1.5.1/docs/windows/000077500000000000000000000000001401367561700161445ustar00rootroot00000000000000liquidctl-1.5.1/docs/windows/running-your-first-command-line-program.md000066400000000000000000000154551401367561700263070ustar00rootroot00000000000000# Running your first command-line program The command line is very straightforward: it was a precursor to the graphical user interfaces (GUIs) we are so used to, so it is much more simple and explicit than a GUI. A shell/terminal like Windows Command Prompt or Powershell is just some place were you write what you want some program to do. And they come with a few build-in "special programs" like `cd` (change directory). Unlike GUIs, command-line programs simply output their results to the terminal they were called from. I will get to "how to install" in a bit, but for now let us assume liquidctl has already been set up. If you want to list all devices, just type and hit enter: liquidctl list And this will result in an output that looks similar to: Device ID 0: NZXT Smart Device (V1) Device ID 1: NZXT Kraken X (X42, X52, X62 or X72) If you want to list all devices showing a bit more information: liquidctl list --verbose If you want to initialize all devices (which you should!): liquidctl initialize all If you want to show the status information: liquidctl status To change say the pump speed to 42%: liquidctl set pump speed 42 This last command will not show any output. This is normal: command-line programs tend to follow a convention that simplifies chaining programs and automating things with scripts: (unless explicitly requested otherwise), only output useful information or error messages. Some liquidctl commands can get slightly less English-looking than what was showed above, but they should still be readable. For example, to set the fans to follow the profile defined by three points (25°C -> 10%), (30°C -> 50%), (40°C -> 100%), execute: liquidctl set fan speed 25 10 30 50 40 100 While in isolation these numbers are not very self explanatory, they are simply the pairs of temperature and corresponding duty values: liquidctl set fan speed 25 10 30 50 40 100 ^^^^^ ^^^^^ ^^^^^^ pairs of temperature (°C) -> duty (%) _(The profiles run on the device, and therefore can only refer to the internal liquid temperature sensor)._ Each device family has a guide that can be found in the [list of _Supported Devices_], and that lists all features and attributes that are supported by those devices, as well as examples. [list of _Supported Devices_]: ../../README.md#supported-devices ## Setting up liquidctl First, there is no installer (for a number of reasons that are not very important right now). But there are pre-built executables so you do not need to worry about installing Python and libraries. It is also the easiest way for non-programmers to use liquidctl on Windows, so it is what I am going to cover here (see [notes] for how to install it the Pythonic way). [notes]: #notes Next, you should know about the concept of the `PATH` variable. This is how the shell, such as Windows Command Prompt or Powershell, finds executables when you type `liquidctl`. The idea is that PATH (this environment variable you can configure) is a list of directories with programs that you want the shells to find; in Windows, directories in `PATH` are separated by semicolons. You can read more about the `PATH` variable in its [Wikipedia entry]. Unfortunately I cannot find any up-to-date guide from Microsoft on how to set the `PATH` (apparently MS thinks you are supposed to just guess where the setting for it is and how it works), so instead take a look at this [guide from Aseem Kishore]. [Wikipedia entry]: https://en.wikipedia.org/wiki/PATH_(variable) [guide from Aseem Kishore]: https://helpdeskgeek.com/windows-10/add-windows-path-environment-variable/ Anyway, getting back to running programs on a shell. If an executable (`liquidctl.exe`) is in a directory that is listed in your `PATH` variable, then typing liquidctl in any shell (like Windows Command Program) will just work. You do not even need the ".exe" suffix. It is also possible to run programs not in the directories listed in `PATH` (this is commonly referred to as running "programs not in PATH"): you just need to specify the complete absolute or relative path to the executable. So there are three ways of setting up `liquidctl.exe`: * Place it somewhere sensible (personally I use the base `C:\Program Files\` directory) and make sure that directory is in the PATH (which it normally is not). * Place it somewhere already in the PATH; I don't recommend this because in most cases you would be placing it into an internal Windows directory, or in the directory of some other program. * Place it anywhere you like and either navigate to it via the shell or specify relative/absolute paths to it; I don't recommend this because it is annoying and not how command-lines/shells are supposed to be used. The last stable version of liquidctl can be found in the [_Releases_ page]. [_Releases_ page]: https://github.com/liquidctl/liquidctl/releases New drivers may not yet be a part of a stable release (which is the case with the Kraken X53 as of 24 June 2020). If that's the case, you can use one of the automatic builds of the code we are working on. All code in the repository is automatically tested and built for Windows. The executable for the latest code in the main code branch can be found at [current build]. You can also browse all recent builds for all branches and features been worked on in the [build history]; the executables are in the "artifacts" tab. [current build]: https://ci.appveyor.com/project/jonasmalacofilho/liquidctl/branch/master/artifacts [build history]: https://ci.appveyor.com/project/jonasmalacofilho/liquidctl/history ## Final words While you should be able to use liquidctl with just these tips, I still recommend you take a look at the rest of the [README], the documents in [docs] and, also, the output of: liquidctl --help [README]: ../../README.md [docs]: .. ## Notes ### The Pythonic way to install liquidctl To install liquidctl the Pythonic way, first install Python (3.9 recommended), with the option to add Python to PATH. Then install liquidctl using pip: pip install liquidctl Finally, install libusb, which unfortunately has to be done manually. The libusb DLLs can be found in [libusb/releases](https://github.com/libusb/libusb/releases) (part of the `libusb-.7z` files) and the appropriate (e.g. MS64) `.dll` and `.lib` files should be extracted to the system or python installation directory (e.g. `C:\Windows\System32` or `C:\Python39`). ### About this document This document was originally a response to a direct message: > Hi. How are you? Hope you're staying safe and well. I just wanted to know of > there is a windows gui for liquidctl? > I have zero experience with command line stuff and I don't entirely understand > it... also most of the guides are from late 2018 or early 2019. > And i just bought a x53 kraken. liquidctl-1.5.1/extra/000077500000000000000000000000001401367561700146455ustar00rootroot00000000000000liquidctl-1.5.1/extra/completions/000077500000000000000000000000001401367561700172015ustar00rootroot00000000000000liquidctl-1.5.1/extra/completions/liquidctl.bash000066400000000000000000000176531401367561700220460ustar00rootroot00000000000000#!/usr/bin/env bash # Bash completions for liquidctl. # # Requires bash-completion.[1] # # Users can place this file in the `completions` subdir of # $BASH_COMPLETION_USER_DIR (defaults to `$XDG_DATA_HOME/bash-completion` or # `~/.local/share/bash-completion` if $XDG_DATA_HOME is not set). # # Distros should instead use the directory returned by # pkg-config --variable=completionsdir bash-completion # # See [1] for more information. # # [1] https://github.com/scop/bash-completion # # Copyright (C) 2020-2020 Marshall Asch # SPDX-License-Identifier: GPL-3.0-or-later # logging method _e() { echo "$1" >> log; } _list_bus_options () { liquidctl list -v | grep 'Bus:' | cut -d ':' -f 2 | sort -u } _list_vendor_options () { liquidctl list -v | grep 'Vendor ID:' | cut -d ':' -f 2 | cut -c4-6 | sort -u } _list_product_options () { liquidctl list -v | grep 'Product ID:' | cut -d ':' -f 2 | cut -c4-6 | sort -u } _list_device_options () { liquidctl list | cut -d ' ' -f 3 | cut -d ':' -f 1 | sort -u } _list_match_options () { liquidctl list | cut -d ':' -f 2 | sort -u | awk '{gsub(/\(|\)/,"",$0); print tolower($0)}' } _list_pick_options () { num=$(liquidctl list | wc -l) seq $num } _list_release_options () { liquidctl list -v | grep 'Release number:' | cut -d ':' -f 2 | sort -u } _list_address_options () { liquidctl list -v | grep 'Address:' | cut -d ':' -f 2 | sort -u } _list_port_options () { liquidctl list -v | grep 'Port:' | cut -d ':' -f 2 | sort -u } _list_serial_options () { liquidctl list -v | grep 'Serial number:' | cut -d ':' -f 2 | sort -u } _liquidctl_main() { local commands=" set initialize list status " local boolean_options=" --verbose -v --debug -g --version --help --single-12v-ocp --legacy-690lc --non-volatile " local options_with_args=" --match -m --pick -n --vendor --product --release --serial --bus --address --usb-port --device -d --speed --time-per-color --time-off --alert-threshold --alert-color --pump-mode --unsafe --direction --start-led --maximum-leds --temperature-sensor " # generate options list and remove any flag that has already been given # note this will note remove the short and long versions options=($options_with_args $boolean_options) for i in "${!options[@]}"; do if [[ "${COMP_WORDS[@]}" =~ "${options[i]}" ]]; then unset 'options[i]' fi done; options=$(echo "${options[@]}") # This part will check if it is currently completing a flag local previous=$3 local cur="${COMP_WORDS[COMP_CWORD]}" case "$previous" in --vendor) COMPREPLY=($(compgen -W "$(_list_vendor_options)" -- "$cur")) return ;; --product) COMPREPLY=($(compgen -W "$(_list_product_options)" -- "$cur")) return ;; --bus) COMPREPLY=($(compgen -W "$(_list_bus_options)" -- "$cur")) return ;; --address) COMPREPLY=($(compgen -W "$(_list_port_options)" -- "$cur")) return ;; --match | -m ) COMPREPLY=($(compgen -W "$(_list_match_options)" -- "$cur")) return ;; --pick | -n ) COMPREPLY=($(compgen -W "$(_list_pick_options)" -- "$cur")) return ;; --device | -d ) COMPREPLY=($(compgen -W "$(_list_device_options)" -- "$cur")) return ;; --release) COMPREPLY=($(compgen -W "$(_list_release_options)" -- "$cur")) return ;; --serial) COMPREPLY=($(compgen -W "$(_list_serial_options)" -- "$cur")) return ;; --usb-port) COMPREPLY=($(compgen -W "$(_list_port_options)" -- "$cur")) return ;; --pump-mode) COMPREPLY=($(compgen -W "balanced quiet extreme" -- "$cur")) return ;; --* | -[a-z]*1) COMPREPLY=() return ;; esac # This will handle auto completing arguments even if they are given at the end of the command case "$cur" in -*) COMPREPLY=($(compgen -W "$options" -- "$cur")) return ;; esac local i=1 cmd # find the subcommand - first word after the flags while [[ "$i" -lt "$COMP_CWORD" ]] do local s="${COMP_WORDS[i]}" case "$s" in --help | --version) COMPREPLY=() return ;; -*) ;; initialize | list | status | set ) cmd="$s" break ;; esac (( i++ )) done if [[ "$i" -eq "$COMP_CWORD" ]] then COMPREPLY=($(compgen -W "$commands $options" -- "$cur")) return # return early if we're still completing the 'current' command fi # we've completed the 'current' command and now need to call the next completion function # subcommands have their own completion functions case "$cmd" in list) COMPREPLY="" ;; initialize) _liquidctl_initialize_command ;; status) COMPREPLY="" ;; set) _liquidctl_set_command ;; *) ;; esac } _liquidctl_initialize_command () { local i=1 subcommand_index # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" case "$s" in all) subcommand_index=$i break ;; esac (( i++ )) done if [[ "$i" -eq "$COMP_CWORD" ]] then local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=($(compgen -W "all" -- "$cur")) return # return early if we're still completing the 'current' command fi local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=() #($(compgen -W "all" -- "$cur")) } _liquidctl_set_command () { local i=1 subcommand_index is_fan=-1 # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" case "$s" in fan[0-9] | fan | pump) subcommand_index=$i is_fan=1 break ;; led[0-9] | led | sync | ring | logo | external) subcommand_index=$i is_fan=0 break ;; esac (( i++ )) done # check if it is a fan or an LED that is being set if [[ "$is_fan" -eq "1" ]] then _liquidctl_set_fan elif [[ "$is_fan" -eq "0" ]] then _liquidctl_set_led else # no trailing space here so that the fan number can be appended compopt -o nospace # possibly use some command here to get a list of all the possible channels from liquidctl local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=($(compgen -W "fan fan1 fan2 fan3 fan4 fan5 fan6 led led1 led2 led3 led4 led5 led6 pump sync ring logo external" -- "$cur")) fi } _liquidctl_set_fan () { local i=1 found=0 # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" if [[ "$s" = "speed" ]]; then found=1 break fi (( i++ )) done # check if it is a fan or an LED that is being set if [[ $found = 1 ]]; then COMPREPLY="" else COMPREPLY="speed" fi } _liquidctl_set_led () { local i=1 found=0 # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" if [[ "$s" = "color" ]]; then found=1 break fi (( i++ )) done # check if it is a fan or an LED that is being set if [[ $found = 1 ]]; then COMPREPLY="" else COMPREPLY="color" fi } complete -F _liquidctl_main liquidctl liquidctl-1.5.1/extra/contrib/000077500000000000000000000000001401367561700163055ustar00rootroot00000000000000liquidctl-1.5.1/extra/contrib/liquidctlfan/000077500000000000000000000000001401367561700207645ustar00rootroot00000000000000liquidctl-1.5.1/extra/contrib/liquidctlfan/README.md000066400000000000000000000074161401367561700222530ustar00rootroot00000000000000# liquidctlfan a wrapper script for liquidctl to control your fans When I built my first water-cooled PC a few months ago, using Linux, I thought it would be easy to control or regulate it. I quickly found liquidctl on Github and after a few weeks I could control my NZXT X73 under Linux. Thanks a lot for that! Unfortunately I found out that I couldn't control the fans in relation to the CPU temperature. (see https://github.com/liquidctl/liquidctl/issues/118) ## Components liquidctlfan - Wrapper script as a small demon systemd unit file - Unit file for integration into systemd ## Prerequisites The tools bc, sensors, liquidctl and logger must be installed. Sensors must receive a basic configuration before it can be used. Please use sensors-detect for this. The setup takes some time depending on the system. For example, after installation, a Ryzen 9 looks like this: ``` k10temp-pci-00c3 Adapter: PCI adapter Tdie: +42.0°C (high = +70.0°C) Tctl: +42.0°C ``` The script expects a line like Tdie. The script has been tested under Linux Mint 19.3. ## Installation Copy the file liquidctlfan into the directory "/usr/local/bin/" and set the permissions if necessary. ## Configuration and usage ``` Usage: ./liquidctlfan -p | --product is the product id of your fan controller (e.g. 0x1711) [*] -u | --unit Celsius or Fahrenheit (e.g. c|C|Celsius|f|F|Fahrenheit) [*] -ct1| --cputemp1 CPU temperature threshold value lowest (e.g. 50.0) [*] -ct2| --cputemp2 CPU temperature threshold value (e.g. 60.0) [*] -ct3| --cputemp3 CPU temperature threshold value (e.g. 70.0) [*] -ct4| --cputemp4 CPU temperature threshold value highest (e.g. 80.0) [*] -f0|--fan0 Fan setpoint in percent (e.g. 30) [*] -f1|--fan1 Fan setpoint in percent (e.g. 40) [*] -f2|--fan2 Fan setpoint in percent (e.g. 50) [*] -f3|--fan3 Fan setpoint in percent (e.g. 80) [*] -f4|--fan4 Fan setpoint in percent (e.g. 100) [*] -i|--interval CPU temperature check time in seconds (e.g. 10) [*] -l|--log Enable syslog logging (e.g. enable|disable|ENABLE|DISABLE) [*] -a|--about Show about message -h|--help Show this message [*] mandatory parameter ``` A normal call could be ... `./liquidctlfan -p 0x1711 -u c -ct1 50.0 -ct2 60.0 -ct3 70.0 -ct4 80.0 -f0 30 -f1 40 -f2 50 -f3 80 -f4 100 -i 10 -l disable` If it is not desired to pass parameters, all parameters can be stored permanently in the script. Just activate the parameters in the configuration area of the script (remove #). ``` ### Enable configuration to disable parameter handling #### ### Product ID of HUE Grid #PRID="0x1711" ##Unit Celsius C or Fahrenheit F #UNIT="C" ### CPU temperature threshold values #CPUT1="50.0" #CPUT2="60.0" #CPUT3="70.0" #CPUT4="80.0" ### FAN setpoints #FAN0="30" #FAN1="40" #FAN2="50" #FAN3="80" #FAN4="100" ###Interval check time #SLTIME="10" ###Enable Syslog #SYSLOG="enable" ########################################################### ``` ## systemd In the directory systemd you will find the unit file. Copy the file with root access liquidctlfan.service into /etc/systemd/system. Please regard if the parameters are to be transferred or if the stored parameters are to be taken over. The appropriate line must be activated. ``` [Unit] Description=liquidctl Fan Control After=liquidcfg.service [Service] ## Fixed configuration #ExecStart=/usr/local/bin/liquidctlfan ## Handover parameters ExecStart=/usr/local/bin/liquidctlfan -p 0x1711 -u c -ct1 50.0 -ct2 60.0 -ct3 70.0 -ct4 80.0 -f0 30 -f1 40 -f2 50 -f3 80 -f4 100 -i 10 -l enable Restart=on-failure [Install] WantedBy=multi-user.target ``` Use the following commands to reload the configuration, load the daemon at startup, start or stop the daemon. ``` systemctl daemon reload systemctl enable liquidctlfan.service systemctl start liquidctlfan.service systemctl stop liquidctlfan.service ``` liquidctl-1.5.1/extra/contrib/liquidctlfan/liquidctlfan000077500000000000000000000203571401367561700234000ustar00rootroot00000000000000#!/bin/bash # Place this script in /usr/local/bin or you've to adjust systemd unit file. # Please have a look at cputemp. You may have to adjust something for your system. # # Copyright (C) 2020 Martin Burgholte # SPDX-License-Identifier: GPL-3.0-or-later NAME="liquidctl Fan Control" PROCNAME=`basename "$0"` ### Enable configuration to disable parameter handling #### ### Product ID of HUE Grid #PRID="0x1711" ###Unit Celsius C or Fahrenheit F #UNIT="C" ### CPU temperature threshold values #CPUT1="50.0" #CPUT2="60.0" #CPUT3="70.0" #CPUT4="80.0" ### FAN setpoints #FAN0="30" #FAN1="40" #FAN2="50" #FAN3="80" #FAN4="100" ###Interval check time #SLTIME="10" ###Enable Syslog #SYSLOG="enable" ########################################################### programs=(bc sensors liquidctl logger) for program in "${programs[@]}"; do if ! command -v "$program" > /dev/null 2>&1; then echo "The following application: $program is missing. Please check it first." exit 1 fi done while [[ $# -gt 0 ]] do key="$1" case $key in -p|--product) PRID="$2" shift ;; -u|--unit) UNIT="$2" shift ;; -ct1|--cputemp1) CPUT1="$2" shift ;; -ct2|--cputemp2) CPUT2="$2" shift ;; -ct3|--cputemp3) CPUT3="$2" shift ;; -ct4|--cputemp4) CPUT4="$2" shift ;; -f0|--fan0) FAN0="$2" shift ;; -f1|--fan1) FAN1="$2" shift ;; -f2|--fan2) FAN2="$2" shift ;; -f3|--fan3) FAN3="$2" shift ;; -f4|--fan4) FAN4="$2" shift ;; -i|--interval) SLTIME="$2" shift ;; -l|--log) SYSLOG="$2" shift ;; -a|--about) echo -e "$NAME - wrapper script for liquidctl to control your fans\nCopyright (C) 2020 Martin Burgholte\nSPDX-License-Identifier: GPL-3.0-or-later" exit 0 ;; -h|--help) echo -e "Usage: $0\n\t-p|--product Product id of controller \n\t-u|--unit Celsius or Fahrenheit \n\t-ct1|--cputemp1 CPU temperature threshold value lowest \n\t-ct2|--cputemp2 CPU temperature threshold value\n\t-ct3|--cputemp3 CPU temperature threshold value \n\t-ct4|--cputemp4 CPU temperature threshold value highest \n\t-f0|--fan0 Fan setpoint in percent lowest (idle) \n\t-f1|--fan1 Fan setpoint in percent \n\t-f2|--fan2 Fan setpoint in percent \n\t-f3|--fan3 Fan setpoint in percent \n\t-f4|--fan4 Fan setpoint in percent highest\n\t-i|--interval CPU temperature check time\n\t--l|--log Enable syslog logging \n\t-a|--about Show about message \n\t-h|--help Show this message" exit 0 ;; *) echo -e "Usage: $0\n\t-p|--product Product id of controller \n\t-u|--unit Celsius or Fahrenheit \n\t-ct1|--cputemp1 CPU temperature threshold value lowest \n\t-ct2|--cputemp2 CPU temperature threshold value\n\t-ct3|--cputemp3 CPU temperature threshold value \n\t-ct4|--cputemp4 CPU temperature threshold value highest \n\t-f0|--fan0 Fan setpoint in percent lowest (idle) \n\t-f1|--fan1 Fan setpoint in percent \n\t-f2|--fan2 Fan setpoint in percent \n\t-f3|--fan3 Fan setpoint in percent \n\t-f4|--fan4 Fan setpoint in percent highest\n\t-i|--interval CPU temperature check time\n\t--l|--log Enable syslog logging \n\t-a|--about Show about message \n\t-h|--help Show this message" exit 1 ;; esac shift done # Parameter check if [[ -z $PRID ]] || [[ -z $UNIT ]] || [[ -z $CPUT1 ]] || [[ -z $CPUT2 ]] || [[ -z $CPUT3 ]] || [[ -z $CPUT4 ]] || [[ -z $FAN0 ]] || [[ -z $FAN1 ]] || [[ -z $FAN2 ]] || [[ -z $FAN3 ]] || [[ -z $FAN4 ]] || [[ -z $SLTIME ]] || [[ -z $SYSLOG ]] then echo -e "Missing parameter!\nUsage: $0\n\t-p|--product Product id of controller \n\t-u|--unit Celsius or Fahrenheit \n\t-ct1|--cputemp1 CPU temperature threshold value lowest \n\t-ct2|--cputemp2 CPU temperature threshold value\n\t-ct3|--cputemp3 CPU temperature threshold value \n\t-ct4|--cputemp4 CPU temperature threshold value highest \n\t-f0|--fan0 Fan setpoint in percent lowest (idle) \n\t-f1|--fan1 Fan setpoint in percent \n\t-f2|--fan2 Fan setpoint in percent \n\t-f3|--fan3 Fan setpoint in percent \n\t-f4|--fan4 Fan setpoint in percent highest\n\t-i|--interval CPU temperature check time\n\t--l|--log Enable syslog logging \n\t-a|--about Show about message \n\t-h|--help Show this message" exit 1 fi # Unit check case $UNIT in f|F|Fahrenheit) # echo -n "Fahrenheit" UNIT="F" ;; c|C|Celsius) # echo -n "Celsius" UNIT="C" ;; *) echo -e "Wrong unit parameter! Please use for\n\t°C -> c|C|Celsius or\n\t°F -> f|F|Fahrenheit." exit 1 ;; esac # Syslog check case $SYSLOG in enable|ENABLE|true|TRUE) SYSLOG="1" ;; disable|DISABLE|false|FALSE) SYSLOG="0" ;; *) echo -e "Wrong syslog parameter! Please use for\n\tactive -> enable|ENABLE|TRUE|true or\n\tinactive -> disable|DISABLE|FALSE|false" exit 1 ;; esac # CPU temperature threshold value check if (( $(echo "$CPUT1 < $CPUT2" | bc -l) )) then if (( $(echo "$CPUT2 < $CPUT3" | bc -l) )) then if (( $(echo "$CPUT3 < $CPUT4" | bc -l) )) then echo -e "CPU temperature thresholds looking good." else echo -e "CPU temperature threshold error! CPU temperature t3 $CPUT3 is not less than t4 $CPUT4. Please check your threshold." exit 1 fi else echo -e "CPU temperature threshold error! CPU temperature t2 $CPUT2 is not less than t3 $CPUT3. Please check your threshold." exit 1 fi else echo -e "CPU temperature threshold error! CPU temperature t1 $CPUT1 is not less than t2 $CPUT2. Please check your threshold." exit 1 fi # FAN setpoint check if (( $(echo "$FAN0 < $FAN1" | bc -l) )) then if (( $(echo "$FAN1 < $FAN2" | bc -l) )) then if (( $(echo "$FAN2 < $FAN3" | bc -l) )) then if (( $(echo "$FAN3 < $FAN4" | bc -l) )) then echo -e "FAN setpoints looking good." else echo -e "FAN setpoint error! FAN setpoint f3 $FAN3 is not less than f4 $FAN4. Please check your setpoints." exit 1 fi else echo -e "FAN setpoint error! FAN setpoint f2 $FAN2 is not less than f3 $FAN3. Please check your setpoints." exit 1 fi else echo -e "FAN setpoint error! FAN setpoint f1 $FAN1 is not less than f2 $FAN2. Please check your setpoints." exit 1 fi else echo -e "FAN setpoint error! FAN setpoint f0 $FAN0 is not less than f1 $FAN1. Please check your setpoints." exit 1 fi LIQBIN=`command -v liquidctl` while true do # ATTENTION Check your CPU temperature sensor value. if [ $UNIT == "F" ] then cputemp=`sensors -f | grep Tdie | awk '{ print $2 }' | sed -e s/°$UNIT//g | sed -e s/+//g` else cputemp=`sensors | grep Tdie | awk '{ print $2 }' | sed -e s/°$UNIT//g | sed -e s/+//g` fi if (( $(echo "$cputemp > $CPUT4" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN4 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN4 %\r" fi $LIQBIN --product $PRID set sync speed $FAN4 elif (( $(echo "$cputemp >= $CPUT3" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN3 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN3 %\r" fi $LIQBIN --product $PRID set sync speed $FAN3 elif (( $(echo "$cputemp >= $CPUT2" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN2 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN2 %\r" fi $LIQBIN --product $PRID set sync speed $FAN2 elif (( $(echo "$cputemp >= $CPUT1" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp°$UNIT setting FANs to $FAN1 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN1 %\r" fi $LIQBIN --product $PRID set sync speed $FAN1 elif (( $(echo "$cputemp < $CPUT1" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN0 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN0 %\r" fi $LIQBIN --product $PRID set sync speed $FAN0 fi sleep $SLTIME done liquidctl-1.5.1/extra/contrib/liquidctlfan/systemd/000077500000000000000000000000001401367561700224545ustar00rootroot00000000000000liquidctl-1.5.1/extra/contrib/liquidctlfan/systemd/liquidctlfan.service000066400000000000000000000005541401367561700265210ustar00rootroot00000000000000[Unit] Description=liquidctl Fan Control After=liquidcfg.service [Service] ## Fixed configuration #ExecStart=/usr/local/bin/liquidctlfan ## Handover parameters ExecStart=/usr/local/bin/liquidctlfan -p 0x1711 -u c -ct1 50.0 -ct2 60.0 -ct3 70.0 -ct4 80.0 -f0 30 -f1 40 -f2 50 -f3 80 -f4 100 -i 10 -l enable Restart=on-failure [Install] WantedBy=multi-user.target liquidctl-1.5.1/extra/krakencurve-poc000077500000000000000000000173341401367561700177020ustar00rootroot00000000000000#!/usr/bin/env python3 """Adjust Kraken X42/X52/X62/X72 speeds dynamically, with software. Deprecated proof of concept, use ./yoda instead. Usage: krakencurve-poc [options] show-sensors krakencurve-poc [options] control --pump --fan krakencurve-poc --help krakencurve-poc --version Options: --pump Profile to use for the pump --fan Profile to use for the fan --pump-sensor Select alternate sensor for pump speed --fan-sensor Select alternate sensor for fan speed --interval Update interval in seconds [default: 2] -v, --verbose Output additional information -g, --debug Show debug information on stderr --version Display the version number --help Show this message Requirements: all platforms liquidctl, including the Python APIs (pip install liquidctl) Linux/FreeBSD psutil (pip install psutil) macOS iStats (gem install iStats) Windows none, system sensors not yet supported Changelog: 0.0.3 Fix casing of log and error messages 0.0.2 MacOS support for iStats; breaking refresh of the CLI and sensor names 0.0.1 Initial proof-of-concept; system sensors only supported on Linux Copyright (C) 2018–2021 Jonas Malaco SPDX-License-Identifier: GPL-3.0-or-later """ import ast import logging import sys import time from docopt import docopt from liquidctl.driver.kraken_two import KrakenTwoDriver from liquidctl.util import normalize_profile, interpolate_profile if sys.platform == 'darwin': import re import subprocess elif sys.platform.startswith('linux') or sys.platform.startswith('freebsd'): import psutil VERSION = '0.0.3' LOGGER = logging.getLogger(__name__) LIQUID_SENSOR = 'kraken.coolant' def read_sensors(cooler): sensors = {} if cooler: data = {k: v for k, v, u in cooler.get_status()} sensors[LIQUID_SENSOR] = data['Liquid temperature'] if sys.platform == 'darwin': istats_stdout = subprocess.check_output(['istats']).decode('utf-8') for line in istats_stdout.split('\n'): if line.startswith('CPU'): cpu_temp = float(re.search(r'\d+\.\d+', line).group(0)) sensors['istats.cpu'] = cpu_temp break elif sys.platform.startswith('linux') or sys.platform.startswith('freebsd'): for m, li in psutil.sensors_temperatures().items(): for label, current, _, _ in li: sensors['{}.{}'.format(m, label.lower().replace(' ', '_'))] = current return sensors def show_sensors(cooler): print('{:<60} {:>18}'.format('Sensor identifier', 'Temperature')) print('-' * 80) sensors = read_sensors(cooler) for k, v in sensors.items(): if k == LIQUID_SENSOR: k = k + ' [default]' print('{:<70} {:>6}{}'.format(k, v, '°C')) def parse_profile(arg, mintemp, maxtemp, minduty=0, maxduty=100): """Parse, validate and normalize a temperature–duty profile. >>> parse_profile('(20,30),(30,50),(34,80),(40,90)', 0, 60, 25, 100) [(20, 30), (30, 50), (34, 80), (40, 90), (60, 100)] >>> parse_profile('35', 0, 60, 25, 100) [(0, 35), (59, 35), (60, 100)] The profile is validated in structure and acceptable ranges. Duty is checked against `minduty` and `maxduty`. Temperature must be between `mintemp` and `maxtemp`. >>> parse_profile('(20,30),(50,100', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Profile must be comma-separated (temperature, duty) tuples >>> parse_profile('(20,30),(50,100,2)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Profile must be comma-separated (temperature, duty) tuples >>> parse_profile('(20,30),(50,97.6)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Duty must be integer number between 25 and 100 >>> parse_profile('(20,15),(50,100)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Duty must be integer number between 25 and 100 >>> parse_profile('(20,30),(70,100)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Temperature must be integer number between 0 and 60 """ try: val = ast.literal_eval('[' + arg + ']') if len(val) == 1 and isinstance(val[0], int): # for arg == '' set fixed duty between mintemp and maxtemp - 1 val = [(mintemp, val[0]), (maxtemp - 1, val[0])] except: raise ValueError('profile must be comma-separated (temperature, duty) tuples') for step in val: if not isinstance(step, tuple) or len(step) != 2: raise ValueError('profile must be comma-separated (temperature, duty) tuples') temp, duty = step if not isinstance(temp, int) or temp < mintemp or temp > maxtemp: raise ValueError('temperature must be integer between {} and {}'.format(mintemp, maxtemp)) if not isinstance(duty, int) or duty < minduty or duty > maxduty: raise ValueError('duty must be integer between {} and {}'.format(minduty, maxduty)) return normalize_profile(val, critx=maxtemp) def control(cooler, pump_profile, fan_profile, update_interval, pump_sensor, fan_sensor): LOGGER.info('pump following sensor %s and profile %s', pump_sensor, str(pump_profile)) LOGGER.info('fan following sensor %s and profile %s', fan_sensor, str(fan_profile)) while True: sensors = read_sensors(cooler) LOGGER.info('pump control (%s): %.1f°C, fan control (%s): %.1f°C', pump_sensor, sensors[pump_sensor], fan_sensor, sensors[fan_sensor]) pump_duty = interpolate_profile(pump_profile, sensors[pump_sensor]) fan_duty = interpolate_profile(fan_profile, sensors[fan_sensor]) cooler.set_instantaneous_speed('pump', pump_duty) cooler.set_instantaneous_speed('fan', fan_duty) time.sleep(update_interval) if __name__ == '__main__': if len(sys.argv) == 2 and sys.argv[1] == 'doctest': import doctest doctest.testmod(verbose=True) sys.exit(0) args = docopt(__doc__, version='krakencurve-poc v{}'.format(VERSION)) if args['--debug']: args['--verbose'] = True logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(name)s: %(message)s') LOGGER.debug('krakencurve-poc v%s', VERSION) import liquidctl.version LOGGER.debug('liquidctl v%s', liquidctl.version.__version__) elif args['--verbose']: logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') else: logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s') sys.tracebacklimit = 0 device = KrakenTwoDriver.find_supported_devices()[0] device.connect() try: if args['show-sensors']: show_sensors(device) elif args['control']: pump_sensor = args['--pump-sensor'] or LIQUID_SENSOR pump_max_temp = 100 if pump_sensor != LIQUID_SENSOR else 60 fan_sensor = args['--fan-sensor'] or LIQUID_SENSOR fan_max_temp = 100 if fan_sensor != LIQUID_SENSOR else 60 pump_profile = parse_profile(args['--pump'], 0, pump_max_temp, minduty=50) fan_profile = parse_profile(args['--fan'], 0, fan_max_temp, minduty=25) control(device, pump_profile, fan_profile, update_interval=int(args['--interval']), pump_sensor=pump_sensor, fan_sensor=fan_sensor) else: raise Exception('nothing to do') except KeyboardInterrupt: LOGGER.info('stopped by user') finally: device.disconnect() liquidctl-1.5.1/extra/krakenduty-poc000077500000000000000000000070641401367561700175420ustar00rootroot00000000000000#!/usr/bin/env python3 """krakenduty proof of concept – translate Kraken X speeds to duty values This is just a proof of concept. Usage: krakenduty-poc train krakenduty-poc status krakenduty-poc --help krakenduty-poc --version Copyright (C) 2018–2021 Jonas Malaco SPDX-License-Identifier: GPL-3.0-or-later """ from time import sleep import ast from docopt import docopt from liquidctl.driver.kraken_two import KrakenTwoDriver from liquidctl.util import interpolate_profile as interpolate DATAFILE = '.krakenduty-poc' DUTY_STEP = 5 DUTY_SLEEP = 5 DUTY_SAMPLES = 5 def get_speeds(device): status = { k: v for k, v, u in device.get_status() } return (status['Fan speed'], status['Pump speed']) def find_duty_values(training_data, fan_speed, pump_speed): # for now simply interpolate, but this is terrible because it ignores variance fan_duty = interpolate(sorted([(speed, duty) for duty, speed, _ in training_data]), fan_speed) pump_duty = interpolate(sorted([(speed, duty) for duty, _, speed in training_data]), pump_speed) # don't return values outside the allowed bounds to avoid confusion return (min(max(fan_duty, 25), 100), min(max(pump_duty, 50), 100)) def do_train(device): # read current values fan_speed, pump_speed = get_speeds(device) print('starting values: fan = {} rpm, pump = {} rpm'.format(fan_speed, pump_speed)) # train training_data = [] for duty in range(0, 101, DUTY_STEP): # don't worry if duty is off spec, the driver will correct it for channel in ['fan', 'pump']: device.set_fixed_speed(channel, duty) # wait significantly to allow the speed to stabilize sleep(DUTY_SLEEP) # get a few samples because there is some natural variation; # though this might need some delays and, also, depend on the actually # observed variance samples = [get_speeds(device) for i in range(DUTY_SAMPLES)] average = [sum(i)/len(i) for i in zip(*samples)] print('{}% duty: fan = {:.0f} rpm, pump = {:.0f} rpm'.format(duty, *average)) training_data.append([duty] + average) with open(DATAFILE, 'w') as f: f.write(str(training_data)) # (try to) restore the current values fan_duty, pump_duty = find_duty_values(training_data, fan_speed, pump_speed) print('applying fixed values: fan = {}%, pump = {}%'.format(fan_duty, pump_duty)) device.set_fixed_speed('fan', fan_duty) device.set_fixed_speed('pump', pump_duty) def do_status(device): # read training data training_data = [] with open(DATAFILE, 'r') as f: training_data = ast.literal_eval(f.read()) # augment status = [] for k, v, u in device.get_status(): status.append((k, v, u)) if k == 'Fan speed': fan_duty, _ = find_duty_values(training_data, v, 0) status.append(('Fan duty', fan_duty, '%')) elif k == 'Pump speed': _, pump_duty = find_duty_values(training_data, 0, v) status.append(('Pump duty', pump_duty, '%')) # report print('{}'.format(device.description)) for k, v, u in status: print('{:<20} {:>6} {:<3}'.format(k, v, u)) print('') if __name__ == '__main__': args = docopt(__doc__, version='0.0.2') device = KrakenTwoDriver.find_supported_devices()[0] device.connect() try: if args['train']: do_train(device) elif args['status']: do_status(device) else: raise Exception('nothing to do') finally: device.disconnect() liquidctl-1.5.1/extra/linux/000077500000000000000000000000001401367561700160045ustar00rootroot00000000000000liquidctl-1.5.1/extra/linux/71-liquidctl.rules000066400000000000000000000132431401367561700213020ustar00rootroot00000000000000# Rules that grant unprivileged access to devices supported by liquidctl # # Users and distros are encouraged to use these if they want liquidctl to work # without requiring root privileges (possibly with the use of sudo). # # Distros will likely want to place this file in `/usr/lib/udev/rules.d/`, # while users installing this manually SHOULD use `/etc/udev/rules.d/` instead. # # The suggested name for this file is `71-liquidctl.rules`. This was chosen # based on the numbering of other uaccess tagging rule files in my system (not # very scientific, I know, but I could not find any documented policy for # this), as well as the need to let users overrule these rules. # # These rules assume a system with modern versions of systemd/udev, that # support the `uaccess` tag; on older systems the rules can be changed to set # GROUP="plugdev" and MODE="0660" instead. The currently deprecated 'plugdev' # group is not used by default to avoid generating warnings on systems that # have already removed it. # i2c and SMBus # Host SMBus on Intel mainstream/HEDT platforms KERNEL=="i2c-*", DRIVERS=="i801_smbus", TAG+="uaccess" # ASUS Strix 2080 Ti OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e07", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x866a", DRIVERS=="nvidia", TAG+="uaccess" # EVGA GTX 1080 FTW KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b80", ATTRS{subsystem_vendor}=="0x3842", \ ATTRS{subsystem_device}=="0x6286", DRIVERS=="nvidia", TAG+="uaccess" # USB and USB HIDs # Asetek 690LC (also EVGA CLC or NZXT Kraken) SUBSYSTEMS=="usb", ATTRS{idVendor}=="2433", ATTRS{idProduct}=="b200", TAG+="uaccess" # Corsair Commander Pro SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c10", TAG+="uaccess" # Corsair H100i Pro XT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c20", TAG+="uaccess" # Corsair H100i Platinum SE SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c19", TAG+="uaccess" # Corsair H100i Platinum SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c18", TAG+="uaccess" # Corsair H115i Pro XT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c21", TAG+="uaccess" # Corsair H115i Platinum SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c17", TAG+="uaccess" # Corsair H150i Pro XT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c22", TAG+="uaccess" # Corsair HX1000i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c07", TAG+="uaccess" # Corsair HX1200i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c08", TAG+="uaccess" # Corsair HX750i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c05", TAG+="uaccess" # Corsair HX850i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c06", TAG+="uaccess" # Corsair Hydro H100i GTX SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c03", TAG+="uaccess" # Corsair Hydro H100i v2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c09", TAG+="uaccess" # Corsair Hydro H110i GTX SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c07", TAG+="uaccess" # Corsair Hydro H115i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c0a", TAG+="uaccess" # Corsair Hydro H80i GT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c02", TAG+="uaccess" # Corsair Hydro H80i v2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c08", TAG+="uaccess" # Corsair Lighting Node Pro SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c0b", TAG+="uaccess" # Corsair RM1000i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c0d", TAG+="uaccess" # Corsair RM650i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c0a", TAG+="uaccess" # Corsair RM750i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c0b", TAG+="uaccess" # Corsair RM850i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c0c", TAG+="uaccess" # Gigabyte RGB Fusion 2.0 5702 Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="048d", ATTRS{idProduct}=="5702", TAG+="uaccess" # Gigabyte RGB Fusion 2.0 8297 Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="048d", ATTRS{idProduct}=="8297", TAG+="uaccess" # NZXT E500 SUBSYSTEMS=="usb", ATTRS{idVendor}=="7793", ATTRS{idProduct}=="5911", TAG+="uaccess" # NZXT E650 SUBSYSTEMS=="usb", ATTRS{idVendor}=="7793", ATTRS{idProduct}=="5912", TAG+="uaccess" # NZXT E850 SUBSYSTEMS=="usb", ATTRS{idVendor}=="7793", ATTRS{idProduct}=="2500", TAG+="uaccess" # NZXT Grid+ V3 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="1711", TAG+="uaccess" # NZXT HUE 2 Ambient SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2002", TAG+="uaccess" # NZXT HUE 2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2001", TAG+="uaccess" # NZXT Kraken M22 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="1715", TAG+="uaccess" # NZXT Kraken X (X42, X52, X62 or X72) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="170e", TAG+="uaccess" # NZXT Kraken X (X53, X63 or X73) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2007", TAG+="uaccess" # NZXT Kraken Z (Z63 or Z73) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="3008", TAG+="uaccess" # NZXT RGB & Fan Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2009", TAG+="uaccess" # NZXT Smart Device (V1) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="1714", TAG+="uaccess" # NZXT Smart Device V2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2006", TAG+="uaccess" liquidctl-1.5.1/extra/linux/modules-load.conf000066400000000000000000000005071401367561700212420ustar00rootroot00000000000000# Load additional kernel modules at boot # # This file should be installed as: # - (as part of a distro package): `/usr/lib/modules-load.d/liquidctl.conf` # - (manually, by the user): `/etc/modules-load.d/liquidctl.conf` # # See `man 5 modules-load.d` for more information. # load i2c-dev to access SMBus/I²C devices i2c-dev liquidctl-1.5.1/extra/liquiddump000077500000000000000000000060751401367561700167600ustar00rootroot00000000000000#!/usr/bin/env python3 """liquiddump – continuously dump monitoring data from liquidctl devices. This is a experimental script that continuously dumps the status of all available devices to stdout in newline-delimited JSON. Usage: liquiddump [options] liquiddump --help liquiddump --version Options: --interval Update interval in seconds [default: 2] --hid Override API for USB HIDs: usb, hid or hidraw --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --vendor Filter devices by vendor id --product Filter devices by product id --release Filter devices by release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus --pick Pick among many results for a given filter -v, --verbose Output additional information -g, --debug Show debug information on stderr --version Display the version number --help Show this message Examples: liquiddump liquiddump --product 0xb200 liquiddump --interval 0.5 liquiddump > file.jsonl liquiddump | jq -c . Copyright (C) 2019–2021 Jonas Malaco SPDX-License-Identifier: GPL-3.0-or-later """ import json import logging import sys import time import liquidctl.cli as _borrow import usb from docopt import docopt from liquidctl.driver import * LOGGER = logging.getLogger(__name__) if __name__ == '__main__': args = docopt(__doc__, version='0.1.1') frwd = _borrow._make_opts(args) devs = list(find_liquidctl_devices(**frwd)) update_interval = float(args['--interval']) if args['--debug']: args['--verbose'] = True logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(name)s: %(message)s') elif args['--verbose']: logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') else: logging.basicConfig(level=logging.WARNING, format='%(message)s') sys.tracebacklimit = 0 try: for d in devs: LOGGER.info('initializing %s', d.description) d.connect() status = {} while True: for d in devs: try: status[d.description] = d.get_status() except usb.core.USBError as err: LOGGER.warning('failed to read from the device, possibly serving stale data') LOGGER.debug(err, exc_info=True) print(json.dumps(status), flush=True) time.sleep(update_interval) except KeyboardInterrupt: LOGGER.info('canceled by user') except: LOGGER.exception('unexpected error') sys.exit(1) finally: for d in devs: try: LOGGER.info('disconnecting from %s', d.description) d.disconnect() except: LOGGER.exception('unexpected error when disconnecting') liquidctl-1.5.1/extra/old-tests/000077500000000000000000000000001401367561700165635ustar00rootroot00000000000000liquidctl-1.5.1/extra/old-tests/asetek_legacy000077500000000000000000000034541401367561700213170ustar00rootroot00000000000000#!/bin/bash -xe DEVICE="--vendor 0x2433 --product 0xb200 --legacy-690lc" liquidctl $DEVICE $EXTRAOPTIONS list --verbose liquidctl $DEVICE $EXTRAOPTIONS initialize sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 0 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 100 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 100 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 50 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 50 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 75 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 --time-per-color 2 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-off 2 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 --time-off 1 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color fixed 00ff00 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS set logo color blackout sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 --time-off 1 --alert-threshold 0 --alert-color ffff00 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color fixed 00ff00 --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS set logo color blackout --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl-1.5.1/extra/old-tests/asetek_modern000077500000000000000000000034461401367561700213400ustar00rootroot00000000000000#!/bin/bash -xe DEVICE="--vendor 0x2433 --product 0xb200" liquidctl $DEVICE $EXTRAOPTIONS list --verbose liquidctl $DEVICE $EXTRAOPTIONS initialize sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 0 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 100 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 100 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 50 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 20 0 40 100 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 75 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 --time-per-color 2 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-off 2 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 --time-off 1 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color fixed 00ff00 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS set logo color blackout sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 --time-off 1 --alert-threshold 0 --alert-color ffff00 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color fixed 00ff00 --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS set logo color blackout --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl-1.5.1/extra/old-tests/asetek_modern_rainbow000077500000000000000000000010541401367561700230520ustar00rootroot00000000000000#!/bin/bash -xe DEVICE="--vendor 0x2433 --product 0xb200" liquidctl $DEVICE $EXTRAOPTIONS list --verbose liquidctl $DEVICE $EXTRAOPTIONS initialize sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set logo color rainbow sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color rainbow --speed 1 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color rainbow --speed 6 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color rainbow --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl-1.5.1/extra/old-tests/kraken_two000077500000000000000000000061451401367561700206630ustar00rootroot00000000000000#!/bin/bash -xe DEVICE="--vendor 0x1e71 --product 0x170e" hold() { liquidctl $DEVICE $EXTRAOPTIONS ${@:2} sleep $1 } hold 0 list --verbose hold 0 initialize hold 0 status hold 0 set sync color off hold 0 set fan speed 0 hold 2 set pump speed 100 hold 0 status hold 0 set fan speed 100 hold 2 set pump speed 50 hold 0 status hold 0 set fan speed 20 0 40 100 hold 2 set pump speed 20 50 40 100 hold 0 status hold 2 set sync color off hold 0 set logo color fixed ff8000 hold 2 set ring color fixed 00ff80 hold 2 set sync color fixed 0000ff hold 2 set sync color off hold 2 set sync color super-fixed \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff hold 2 set sync color off hold 0 set logo color fading ff8000 00ff80 --speed slowest hold 5 set ring color fading 00ff80 8000ff hold 5 set sync color fading 0000ff ff0000 00ff00 --speed fastest hold 2 set sync color off hold 5 set logo color spectrum-wave --speed slowest hold 5 set ring color spectrum-wave hold 5 set sync color backwards-spectrum-wave --speed fastest hold 2 set sync color off hold 5 set ring color super-wave --speed slowest \ ff8000 00ff80 8000ff 800000 \ 008000 000080 400000 004000 hold 5 set ring color backwards-super-wave --speed fastest \ ff8000 00ff80 8000ff 800000 \ 008000 000080 400000 004000 hold 2 set sync color off hold 3 set ring color marquee-3 ff8000 --speed slower hold 3 set ring color backwards-marquee-3 ff8000 --speed slower hold 3 set ring color marquee-6 00ff80 --speed faster hold 3 set ring color backwards-marquee-6 00ff80 --speed faster hold 2 set sync color off hold 3 set ring color covering-marquee ff8000 00ff80 0080ff --speed slowest hold 3 set ring color covering-backwards-marquee ff8000 00ff80 0080ff --speed fastest hold 2 set sync color off hold 3 set ring color alternating ff8000 00ff80 --speed slowest hold 3 set ring color moving-alternating ff8000 00ff80 hold 3 set ring color backwards-moving-alternating ff8000 00ff80 --speed fastest hold 2 set sync color off hold 0 set logo color breathing ff8000 00ff80 --speed slowest hold 5 set ring color breathing 00ff80 8000ff hold 5 set sync color breathing 0000ff ff0000 00ff00 --speed fastest hold 2 set sync color off hold 5 set sync color super-breathing --speed slower \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff hold 5 set sync color super-breathing --speed faster \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff hold 2 set sync color off hold 0 set logo color pulse ff8000 00ff80 --speed slowest hold 5 set ring color pulse 00ff80 8000ff hold 5 set sync color pulse 0000ff ff0000 00ff00 --speed fastest hold 2 set sync color off hold 5 set ring color tai-chi ff8000 0080ff hold 2 set sync color off hold 3 set ring color water-cooler --speed slower hold 3 set ring color water-cooler --speed faster hold 2 set sync color off hold 3 set ring color loading ff8000 --speed slower hold 3 set ring color loading 00ff80 --speed faster hold 2 set sync color off hold 3 set ring color wings ff8000 --speed slower hold 3 set ring color wings 00ff80 --speed faster hold 2 set sync color off hold 0 status liquidctl-1.5.1/extra/prometheus-liquidctl-exporter000077500000000000000000000115351401367561700226310ustar00rootroot00000000000000#!/usr/bin/env python3 """prometheus-liquidctl-exporter – host a metrics HTTP endpoint with Prometheus formatted data from liquidctl This is an experimental script that collects stats from liquidctl and exposes them as a http://localhost:8098/metrics endpoint in the Prometheus text format. See: https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-example Example metric with labels: # HELP liquidctl liquidctl exported metrics # TYPE liquidctl gauge liquidctl{device="NZXT Kraken X (X42, X52, X62 or X72)",sensor="liquid_temperature",unit="°C"} 33.6 Usage: prometheus-liquidctl-exporter [options] Options: --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --server-port Port for the HTTP /metrics endpoint -v, --verbose Output additional information -g, --debug Show debug information on stderr --version Display the version number --help Show this message Copyright (C) 2020–2021 Alex Berryman, Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import sys import time import usb from docopt import docopt from liquidctl.driver import * from prometheus_client import start_http_server from prometheus_client.core import GaugeMetricFamily, REGISTRY, InfoMetricFamily from datetime import timedelta LOGGER = logging.getLogger(__name__) def gauge_name_sanitize(name): return name.replace(" ", "_").lower() class LiquidCollector(object): def __init__(self): self.description = 'liquidctl exported metrics' def collect(self): labels = ['device', 'sensor', 'unit'] g = GaugeMetricFamily('liquidctl', self.description, labels=labels) i = InfoMetricFamily('liquidctl', self.description, labels=['device']) for d in devs: try: get_status = d.get_status() for metric in get_status: sanitized_name = gauge_name_sanitize(metric[0]) sample_value = metric[1] unit = metric[2] if isinstance(sample_value, timedelta): # cast timedelta into seconds and override the supplied unit sample_value = sample_value.seconds unit = 'seconds' if unit != '': # FIXME doesn't handle multiple equal devices well label_values = [d.description.replace(' (experimental)', ''), sanitized_name, unit] g.add_metric(label_values, value=sample_value) LOGGER.debug( '%s: %s as GaugeMetric %s labels %s', d.description, metric, sanitized_name, '/'.join(label_values)) else: i.add_metric([d.description], value={sanitized_name: sample_value}) LOGGER.debug( '%s: %s InfoMetric labeled with %s => %s', d.description, metric, sanitized_name, sample_value) except usb.core.USBError as err: LOGGER.warning('failed to read from the device, possibly serving stale data') LOGGER.debug(err, exc_info=True) yield g yield i def _make_opts(arguments): options = {} for arg, val in arguments.items(): if val is not None and arg in _PARSE_ARG: opt = arg.replace('--', '').replace('-', '_') options[opt] = _PARSE_ARG[arg](val) return options _PARSE_ARG = { '--legacy-690lc': bool } if __name__ == '__main__': args = docopt(__doc__, version='0.1.1') opts = _make_opts(args) devs = list(find_liquidctl_devices(**opts)) for d in devs: LOGGER.info('initializing %s', d.description) d.connect() if args['--debug']: args['--verbose'] = True logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(name)s: %(message)s') elif args['--verbose']: logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') else: logging.basicConfig(level=logging.WARNING, format='%(message)s') sys.tracebacklimit = 0 REGISTRY.register(LiquidCollector()) if args['--server-port']: server_port = int(args['--server-port']) else: server_port = 8098 start_http_server(server_port) LOGGER.debug('server started on port %s', server_port) try: while True: # Keep HTTP server alive in a loop time.sleep(2) except KeyboardInterrupt: LOGGER.info('canceled by user') finally: for d in devs: try: LOGGER.info('disconnecting from %s', d.description) d.disconnect() except: LOGGER.exception('unexpected error when disconnecting') liquidctl-1.5.1/extra/windows/000077500000000000000000000000001401367561700163375ustar00rootroot00000000000000liquidctl-1.5.1/extra/windows/0001-Fix-203-libusb-sometimes-cleaned-up-too-early.patch000066400000000000000000000035671401367561700302060ustar00rootroot00000000000000From 8041ec11b2c432632814fbadf8af2417a15dfa69 Mon Sep 17 00:00:00 2001 From: Andy Clark Date: Sun, 23 Dec 2018 21:42:47 +0000 Subject: [PATCH] Fix #203: libusb sometimes cleaned up too early. Each call to libusb1's get_backend returns a new _LibUSB object. When each _LibUSB object falls out of scope, it calls ends up calling libusb_exit() which deinitializes libusb, even if there are still references in scope. Practically speaking, pyusb seems to assume there's only one context active, so returning the same _LibUSB object on subsequent calls to get_backend should prevent there from being more than one call to libusb_exit() when backend objects go out of scope. This should fix errors where the libusb library was being used after libusb_exit() had been called. --- usb/backend/libusb1.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/usb/backend/libusb1.py b/usb/backend/libusb1.py index de30c25..968d5b4 100644 --- a/usb/backend/libusb1.py +++ b/usb/backend/libusb1.py @@ -256,6 +256,7 @@ def _get_iso_packet_list(transfer): return list_type.from_address(addressof(transfer.iso_packet_desc)) _lib = None +_lib_object = None def _load_library(find_library=None): # Windows backend uses stdcall calling convention @@ -936,12 +937,14 @@ class _LibUSB(usb.backend.IBackend): return transferred.value def get_backend(find_library=None): - global _lib + global _lib, _lib_object try: if _lib is None: _lib = _load_library(find_library=find_library) _setup_prototypes(_lib) - return _LibUSB(_lib) + if _lib_object is None: + _lib_object = _LibUSB(_lib) + return _lib_object except usb.libloader.LibraryException: # exception already logged (if any) _logger.error('Error loading libusb 1.0 backend', exc_info=False) -- 2.24.0 liquidctl-1.5.1/extra/windows/9999-Add-local-version.patch000066400000000000000000000006451401367561700232510ustar00rootroot00000000000000diff --git a/usb/__init__.py b/usb/__init__.py index d83be87..6ecd412 100644 --- a/usb/__init__.py +++ b/usb/__init__.py @@ -47,7 +47,7 @@ __author__ = 'Wander Lairson Costa' # Use Semantic Versioning, http://semver.org/ version_info = (1, 0, 2) -__version__ = '%d.%d.%d' % version_info +__version__ = '%d.%d.%d+liquidctl.1' % version_info __all__ = ['legacy', 'control', 'core', 'backend', 'util', 'libloader'] liquidctl-1.5.1/extra/windows/LQiNFO.py000066400000000000000000000125221401367561700177430ustar00rootroot00000000000000#!/usr/bin/env python """LQiNFO – export monitoring data from liquidctl devices to HWiNFO. This is a experimental script that exports the status of all available devices to HWiNFO. Usage: LQiNFO.py [options] LQiNFO.py --help LQiNFO.py --version Options: --interval Update interval in seconds [default: 2] --hid Override API for USB HIDs: usb, hid or hidraw --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --vendor Filter devices by vendor id --product Filter devices by product id --release Filter devices by release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus --pick Pick among many results for a given filter -v, --verbose Output additional information to stderr -g, --debug Show debug information on stderr --version Display the version number --help Show this message Examples: python LQiNFO.py python LQiNFO.py --product 0xb200 python LQiNFO.py --interval 0.5 Changelog: 0.0.2 Fix cleanup of registry keys when exiting. 0.0.1 First proof-of-concept. --- LQiNFO – export monitoring data from liquidctl devices to HWiNFO. Copyright (C) 2019–2020 Jonas Malaco yoda is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. yoda is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging import sys import time import winreg import liquidctl.cli as _borrow import usb from docopt import docopt from liquidctl.driver import * LOGGER = logging.getLogger(__name__) if __name__ == '__main__': args = docopt(__doc__, version='0.0.2') frwd = _borrow._make_opts(args) devs = list(find_liquidctl_devices(**frwd)) update_interval = float(args['--interval']) if args['--debug']: args['--verbose'] = True logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(name)s: %(message)s') elif args['--verbose']: logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') else: logging.basicConfig(level=logging.WARNING, format='%(message)s') sys.tracebacklimit = 0 hwinfo_sensor_types = { '°C': 'Temp', 'V': 'Volt', 'rpm': 'Fan', 'A': 'Current', 'W': 'Power', 'dB': 'Other' } hwinfo_custom = winreg.CreateKey(winreg.HKEY_CURRENT_USER, r'Software\HWiNFO64\Sensors\Custom') infos = [] try: for i, d in enumerate(devs): LOGGER.info('Initializing %s', d.description) d.connect() dev_key = winreg.CreateKey(hwinfo_custom, f'{d.description} (liquidctl#{i})') dev_values = [] for j, (k, v, u) in enumerate(d.get_status()): hwinfo_type = hwinfo_sensor_types.get(u, None) if not hwinfo_type: dev_values.append(None) else: sensor_key = winreg.CreateKey(dev_key, f'{hwinfo_type}{j}') winreg.SetValueEx(sensor_key, 'Name', None, winreg.REG_SZ, k) winreg.SetValueEx(sensor_key, 'Unit', None, winreg.REG_SZ, u) winreg.SetValueEx(sensor_key, 'Value', None, winreg.REG_SZ, str(v)) dev_values.append(sensor_key) infos.append((dev_key, dev_values)) # set up dev infos and registry status = {} while True: for i, d in enumerate(devs): dev_key, dev_values = infos[i] try: for j, (k, v, u) in enumerate(d.get_status()): sensor_key = dev_values[j] if sensor_key: winreg.SetValueEx(sensor_key, 'Value', None, winreg.REG_SZ, str(v)) except usb.core.USBError as err: LOGGER.warning('Failed to read from the device, possibly serving stale data') LOGGER.debug(err, exc_info=True) time.sleep(update_interval) except KeyboardInterrupt: LOGGER.info('Canceled by user') except: LOGGER.exception('Unexpected error') sys.exit(1) finally: for d in devs: try: LOGGER.info('Disconnecting from %s', d.description) d.disconnect() except: LOGGER.exception('Unexpected error when disconnecting') for dev_key, dev_values in infos: try: for sensor_key in dev_values: if sensor_key: winreg.DeleteKey(sensor_key, '') winreg.DeleteKey(dev_key, '') except: LOGGER.exception('Unexpected error when cleaning the registry') liquidctl-1.5.1/extra/windows/liquidctl_logo_v1_circle_256.ico000066400000000000000000000724041401367561700243770ustar00rootroot00000000000000 tPNG  IHDR\rf IDATxy$Y;'"3~o[WwuwUWu%ԋZ-оDc$ a7`33F~A$4Rw%oowόssN%".uU'NDƍ;KL]r `? {'l2 <(0 <^^vt1$&}j8 W* dboZ`0?<,MWLfrgYČ;AƀdEct|3 ,D-nA?X!f0.!  A,{Y'p5-^ufЬ7X(L Ɓ `ٜ"QLx6Ii^\袀'oқ&&?8Z1-[R5;mZ6a>[A&X8$'QGQa$}b` 3O_æK$p3y.-'~#CNk-[֠ niR5wQs:'A:g1(яɧ3`19 9+h|+c7֮"#Yjj|je +@u(gy(}=!, "0kYmf~3cToy0B|0,T\mIЁbE3,xه~9{> =6+3f˼ Z&^S sK2G(HxbP"1ԟg3Or{7# 66!nQ&o~@T~*Kղ27B\*(_`3Տgab<>&zwfMF.sLȡ-˸*;}X"۲9FsZWFR~:gmNǼf BFWZnb*&!Ʈ7"Kǭ0A+v*u z2Mq1Ӽ6hP~%>6 #ж އ/N1?w\MǴoKGp/p!V 5Оȷ}ܱU\xF%6:߲q&= Vv$rOBll7}Qd;I?S!m#@ @d2ﭑweu?vtWl|֎[9$zws^\y$J-k~ d7 J<{}c1\n_N{zM^1f eZa?ƠO?`-4]g Vz$YWK׵.LWSЇ*Z#[4wH%C5uI?H`@Kk[~}Œ8]=8( iN"k-YlΉ՘/üEzÄ\cK㇘zϓ uH^K6]ܺB!HzeWAP G&ɯv7IHPߺ ^ ̴L0v?ef Hm*UpHq7#ӺN=@>u%EQN?LbcYt|]aa,1Es@ кyQ@q݁a<{{Sp?[-@9&eo#UMa-d6_Ov \ۧ&Ư`͞r(O˻yۧ^F%%%` 1w!Fqm_oL 4.o%LG"IAm6? m _K:E 3ŕM9/e7 <Pׁ?c]5֪ + vodn8Ʊ$)|xWZGe}W-&Zᅵ, $ ג[ 9yhnF49ֱzMaLdHCodPVA<:(KEK(_'/ e;@=ޘaz~$w֋ivK2$-$B`z(Q(PeS2Md?J~駋~ҬW[u㫸vs6q~˿Z<|bz@u5#bbS.l˯BJI&rJh łiy:ʹ:sL3,ҝyur)ˢ}sBHkHp^<~?Ƶ EC/61Sz-Ʉî%X1 -ljaQ(] Kb,~aAq8Lqc+dVyU)uҿ"闓 _*ۃ/h!CSSVm:fgCS4Ň?ܗKn>碍p=Kl>MlW5H6 Xh[uʝ(Ib>G9yg;_>RՁ?S'OcJ_/\,cP&u]~5Htݬ}Y%Ӟ]|76Oh28b>\W5ux^  ʦBg[&8>vbF~ k up?sGny3u^b2,w $ o *?=$_%H}xߖ_䕣!P]'ױqv/_kCAv8܄Z{&n;RR{^1~C t;?L:`|֚ҝ:,57'/dkd?Va7[~]Cd-a? Jo;> QQ ;y;#8k @ R_ =<* j: Xj!wφ&@6A-37&bK } 15 $nA-ΡN솜|y܅h-eч4ؚ #\v h!:q׵B=aݢbK,npMji3^6 jA-5 9{_A#Z$zk{q| =<,霘f|~{ X0SߎgɲBA~3@rg;xdIO;٘rJlWv<'Sz2'SMAAҝrA'۱ͺp37| ZZ^F:w܇yaxqz{tUz* b%P +5PԄ9V%?'\ݴx;c;LnH M5PG1OmWex33{ leY]_/:j =s$ =ABF]=Ye+{Ta_Oڢ{Yf]Q_QNuۓW)>f~Lq5;~w2i\r>Oȉ!Zdm72 /™omm/Jp!0pα w2e{tŘ2Wnwj02/lg)'U_*h O&@pZ "J | of1j HOE_p|~I n]S~yh >x>x#]1L`k{tTAz̽ewӫ9j @~α*{ifkۿb[oFDQOM|1ty+_qahWK[ǏBCWi]'~JXuj};x)rb؏~]R4=> νf6aϲ Zc1޳WeWWoiz" p%aϿH`u]- ~ |퉁s4O<Qb׮Pa?/eK.}N6LH&Tߕ싼ӀaOA=~m:sF{}a:N؉UL`^y~gB[n)&cNC/ _;%zBOYQ34?Dg5CKi&$シQuA!DN@Te8-Oe}W!] R$JS w3,}sMC%.VCvUYFn&$Y w#OJu$@)})~ Ox艝tm"4͎~xϏi "@RJ [ GBGSDphDS3{ԛ蹙<5ݹ=rmX8=$ 'Xaj0_z?wMcZM -*$7ӂ.eq@HO D3@q=0K=H@1hOpuGIDnCK$~z,K20 7%N f'1<~&fՖBaJ#^I+lGh\=Β/w9ji.i PR0'-l3g&|^ZmWS?ƈC;”Px*A fZ/KvL@_~zx HHJq`8 =APO oƷwG5aȚ5jdru*@%j-8%4YEPz~oN}|Ń[hoнfN2Է߿Mkh{~er 6ɧ#@FO3˕#=T=de u@!DhmBTAx| QD*2V״̓v['=>5>?@!Y-3$j݁(9Gvѝ"}~_W4qX\?js8Ȯl0vP;O]ζweal7rfed`XH>LϿu] 7 B_'DBKG:>>'s UAnI|E1H y<#BY'C!9|(NKOF $Zo" 2#WS"O~Co% =T,Rw1_X^,C}z7u;Z8O,Uq99|?&6% ŝߊDF:~ôY$KM0JoǠۢ*cpğ z.{t, E)݁YԙGX|nO>ads !0@Wׂ? y⼄!#ȉVXjaOn?)qL#9Q?u,^J,èa'=+.e#> ?l}[%dn(õĿpmI $*=eĤPBϘTB*$]EP?u(--3"[Js 0jTqQ|NޱI/pw$R2(1MTAn;'j L^Cofnᛧ~\p2@*VbIMժz~x?Q@ |ҁ?DPT@,'R:n4jH)q@6yZN|@DP0tqty%!䚩=DU]߭kAOȐC}P22kb^͗F.v?6 IDAT;hɑ Nׄ^q}}0/tQW[ɮMeW:%#H`H^ -n-W?Fg86YCB(Su'T!hui@pjYen}1{Yt]o#]M~ӺlbM ݎ r j,$'Vr ZpZ Bkx',0|= ?J *ɠ\H V$4ct>c1/%rݒD;v)] ȉX hYhU^4NBQxJ2˦)_׫Rp8Vq[k|1 ^%؇`!%Bd T!2o'F*wՅW~8'*Xf$ szP܁<]N-;$sh]RM ޤArkzt|p *ľUZzו{@%ꬩMDea1=D~$)~-A֌bB1#>e,V߯JS .*c?sň"_AFwS=qdd.7")ß5YA֩snEZݫ+w~._6Lp*эv\+ZRӍ $HXFl7`;u(vx@>2_.B[/F i] * AbA0V mWa:嫹0_$^i:B&S݀Kj^vhf_g;6s(^RAH9'F. *{Yԑyxޞy2iA@[0  ZF ~Ź8g:G9V<" zE\:24&$q|_%I'SgDle;,hJl)q\it;Rc% b?4%,Kxq{{@a> u[ުa@L"KgA]/S_(+3БDfzjJwSgyn!oSY:Kr-PM[dAKx:EÉݾpS/Gw  NCPDљ`g"޷At%#!:|8bsݾh@ "`xz\Qj'B#u%Zd= <>|y1x4&l)pE2]Pl^-%ʹ)̑״T,.Dm.߅(v-d{&>\G7ӧ|뜪<7r!8Gftߓ*] ZgeF1ydn$g5ɅFgh#EAV&KN#2ǃ>eWz:*ss>P{~r&ݥ[v:2qkRn}C;bW4WnXoX ꎌT\6% L{ 4H3oHKp(zAFFVE(Pwj^q-xs|ڄ3w-K~/nA+/Zn @_/p"ATɾw.n{}?cvk EP@O-gS C+7mE,р^8M7%2{!V$@]՛NʃVr##4RܴnXY27i;P!@^E(F뛙B`(p_Py>*;<09>;J:=8;7wfE`)B=gгmmGSt9_/0/+7Pgld0WֆۀO4Pof?A[/De/خPǿzT>ze>Q+݀w3]_/U\/ 02*Un,uSS% PSNH a(CQ<8|fô7;:zSFwAdw_-.pnf]ӛi egǗ%''`|/u<@J~{gvvWmN*ڡ( H~"+ G2ϙ헬EKNVXTYoQ5*F .ܺ9&Q5HTGxL7q LYS{9@Lc~953gwքg^Hz ɀ)2ΫS|~%c%c<\ ?x,TN B|!zED1QhDe* URY mCLކgt}F}*bԋ@,M"?HS{IR5Nj^&a>wȿr k_8=>Uu; eyz* !N^'k$x9wQ|mi\[P6E4FI/Qq n"f}h@?ʰ0]o4~]/х/`s0(\}C1)Ћk5z~:P<0DHI\Wd[mswhb`~c⁃FsO8zi7Anzbxـ%TP  z{~?sy:* S>旕HTػOl Мp'褾:P Pn4]|;2LF𧁭9} 3l\X/DZ wQ-,MQB+|ft1<$Z]e%MTWҞQg.W5" 깲 2<@$9}<.?vc=}1M1+K9-;)q}]lϯ಼K.R7px>}dAJZזܥF €o.o2Vw`+ \\[`᱁BP/>믻;Ǽ6hSno^ʚ9W\nggay:. gSUpS6啄' "!043x2)ZFHk[ ^=$3Pj!k !X Z,cLAykL(\63_hM^^$r> Brp%жG)uMt}>ߵ#E>k Oȼ0lL_FSw 7S~+{08dѷqޗeWN! 0;w1~{}O}HuR"xGdh9M!Үid<5Jf=Q>w@ws^]_JKȫ/ w=%.L;{! *)qdhxr]VÁeWM!';f& h3mU`W w>)ZFrxaR_cBa=OS<|7mF[d+;O%QF@/8bԂ"|k|@D }UL2Wm5TIBIL߆Y#`KV>L{%lH@oCI ATh(NN3_)l4ˁcl@B4Lfy@t@!еI=}P[0aSҁ\ 2=,*s퍖tL@vperYvJ3 RZ0:/a^@ۖ#=~$u`BNh#7+\/"[k5)W ~7"p@{6O'& N-٠,)5Jc6wxyaδ3cT |Ga0W;w![x Sp!j8z nDl$_ d,h3!*&sbp%ib4'߷]B~wrDfX-m3o/2}sL03B^>䊡Q;"P5D`[czA(=xp6w6wc~DS؆d ~љ4 0l\mڶ>Wb:*Jah;Pw8쎱]WW[* aÁ$DQ0@ re b~_1@l7Kmr;E@Z"2Cш +M=#A$N6GP6@'OOszfc Yž΁~_5C v'ɻAQؓ,mzBKA&b$FZ> tindGpk iL o|2ʑ6p9 ?57D˵xW!;F?I@dz*n`%G,:(JL{ L"t1lzgî#iP>a IDATEn&kXoSmf:Ya9p8Ywí:o .w?f#AH"!w5`󃮿+7]7|lܦ |\$L`$ b 3{G7#NI @{Z ;"pK@Ms[+`UYyFؤ~DXEBS_V:tz) (&2LuÖ9' y~!L;焆Z0f([{vQx@ Ɖ!Zn%M=ΓSIxY&:HH;gB@?ᴉ-a`Za "bp6:b$)\=_5Yp͖S-K Dgf8䑳|78}j?yԹa^t9>M &#Y9هqlNٜA$ki@{ER;}6mA4YyE`?B^/}vj z+_ډ^d#c4uڤ 0[_mV'rĐ$ܺSLCB0ۣ3-\鹄2+*'^Q.@è|K5wfτuumK_fZGؽ%OTj0WM sh9[LRV \I–$tO⧚3ߝlYׯ Q܄Koo)_7NW-ݷÖaw Qcu`_!MH`}st0ZuMC, ܮj5܏J {Q3xR)ݱ7+*S+"HWI YK|_`9x9Iv] pxjm{i ~p!ӗ?3E]"AP$w[}l=Y6<(dϤH 4/P&sإ) t^ 磣t%JJrcT~ʢJǀЈzps.i_TḬ>Ȁe gi RuR()$e+uavK V ͦ!R8 p_rnY:x)[|4cuPd߲Tn_$4A7rRg)wMp\/~t~#zh@0 `hUZ$A ^0HFQ.`o|W7hPH/bc>8kgKzDDzIOAU|mU _Zn ~웧x| 2_rb#:FXWA02V]L-L92$̺ Ü OAДecBe2_UJ;LĴd,K| ozenfm6] MYnw^wNI>)^CmA[t_p= ,!JCn1%ǒg-FvRZӊ Oi-dY 9- PK^Wfq٠0I<-n_}8 7&g x?k3\z.牮3( L}[y苜/;-^'MNU^(l5J3a{E3|kb@);[ @䖳A\5nn1B,+z(9~*x7/bBf˜W/k,ښ>\7<ěFҹ qz]+ o 7N;RpX hўDs7kDvy*y8t)NowbakgT.inC)]8P59s5lh^KC:1d&>ZςA6e2c+-yJ]"\GQ-JⰇo/c 6BƄfsh ]4'Q!{+_pmūGl~n=z:Y&rA 6}4(JG$V!O%e)b!!5\K>9~>fm4xz]Igi?g^ˑi`:Yi8nR8>ڳmu7'QZ /.4@/%[Zh i2) w8XE\69?tynWjqݏs}y:RQBml)܄cPzY늈f\ّ025Ӆqix@t@&;_[G[T~ƽ@R(_/G/qZ> r>\V˧$zw?D_dVRXS~D\oeVOCü8[ac0ۚÂcw-tg1pW/HM!JX@^!!oqL"zkL/b1l_<)O`w=9!**&KkWgݿs!mh϶{#`Y8!,!kj6ɴ~T -# 7M_0?wɸrto2S%MBQ-AWip`a0*YGxbn}Kr=pҕHv g@QA H]<'&c*wKuCtU^ 65Bn_<Ƿ 2,0GXvD~=[%{e|پ{,॥? 4GEZHTnt-ZVv9㙤 VnD`CM3b>&M_\(=kt D\췟mi[ojsy# .(=TA_Bp@y9r__@N2Xu(NnyPri7J,gT 2n3 -xыG%zQ& :̵+&~(׷zGNa]_ M!l4Brg!84OE|y* +#5V޷XU+i]w!kPDMw 㱯[j]K7H]˄ as [40vo`s4%6fط%clH̭צiczeJ}HmhDƫ]€`yE,u:.wf!;K/Y]uz,lHp 9=^iDeKR<0QUeې@NJ@&u|B p۵*s7|ۇJ o_~]?VmwYi(2=W}ҥ'՗y<|?9ޙGYRs_l= 0l2HAH2XJNNDb'rp%|r u,c[R$$` ɀ0 ,=Kwn{-ݼ_{^}w7>"o2*5Oox|_/Qz [([ޤԼ>{IhJ@!<5:8۪ SJHH9d~45Wɇ[I #=6QPӭ<<㿆~M98I@ ţ85!L;CZbύMspb|"H62zD"DH~LO_|_s:1[}5-"kD v hd6$\@ 8v,hF5-^ f*3dqRH\ se(sj%:z{L|s_>4 n~MsWa543}f_A~ұ\*kدl4FEZן 0ԶO^+\%z\|dz(&i=tz[f&.8ϖygi?J4|g raD'[J>n~Oٿ6QBzKDSA}gш Un3<*̶=ɗƲ{^aj$+1h2s"H3s iWͧ\O O/#e׷5gø.go,BMH,س1 $Ջƒ/y]׿z]w,% ̪B&$M8#BO='<эg+"t[`_<ͥgB"%~شȇzw^ԯi2Y_—u3Wo!Bл^_?jdPQ"| iс/D"L-}I ˯r{\ 9v(J| }zj k}w/wZO﷗cmBzdA4cJJ3E?}-mf@b} |985uYP0l#7^oD26)Ƕ{TTi?!=g1$9@m{D25!"U(CI33):"ΟM ڶYua3灜c{ ` Hϐٯl$ h [WZj-m'Bɟq?ޱe๩(SKse{r!5U!Ϳp3&sek|/=as0#$gBy^fXP hi6H}C 'X  |0Z Jf%D!<$?R [XI-Dey #:J7ʚܗX/;La ɃFSrv @)eazsސ(m3 X&2I$#'%I'8-{I H$s v4s0Aדߚ0k%?7Xn`Βy WVmU)0A"YCB}5#`zm&`+{px->F*!V V՟Fğ`,q;|&"NJU;+{˞HcBhd!4#42-ItMi"ٟ)&=R'\`l2=0 \ %z\140@YI޽q KnۣmM I{NRz=~5ށd1:eMh֔ ЀIIK Z^? ,B@;bfnI)=)foi8 |ydu[ ԁZ>ہfBv-sbƞͥJ7(dO>=C8VSҺPIDATۂn_ sEٷ伽dԼo+ cFлZ-G UR|t5`{:ѕ0^,Ѐ0R|3$9R|q>ifZ?f/7.: qJ<`_-EjRdύ{gh=٫Bu? @\;;&q kxAκRߣgKakMAFZ@5[I@k|4ޡlZ|qJ{@O,3;Po#RXc'ر;/ZǍkXᢞKzM~ꜩT>5P( )u,X^Mmkdu+r^[r>ˎӟ_<:[fI/9bdKdJ;H/Ysx3l,"Уu?oy*'7:P!m)_caZ #M4KO֖ƫu$r(IG*LG* t䫌tpl'΅!Tx'{c}m9a'v LVӸEo@J"[I0杀dx,/Ǫ->ݲPu-*.d(RmKR©+ |.j2kJHR‘,y\I#9c?2>nfY Rho7Z_7#x5gǢm@صDx+HF΅yz\3_vk/v> BW"Gvm/gv E0d]Sc_L"Hpq˼2> ?Y"gTJ6-G0FL>ZcW/Uw16F vC4n5PvwVF*4tOR#U~>iζ[oՀOH`Il) `7p-@6p ZvyK# @!:5*|/\.佾RJHm'3U(t) im3B 7~r$wKc瞰_ǁa?ݖO ~K|펳ٓu('ٌDޘCһ!%Gdl7{.U_' W1y<*F贜Ox p~Z"0dz ]iɮ*|i i -ތ|o$nkۀfoD@Y5yoܥD^ (EHpS#Z  6 Y}зx`~vE:KQqK}f5},qZYvb ˮ r$F6 =ͷHUCv /Tϵz[JC,H.t/+ Jr <|;QT%#"Sgi=:3]XFIzUh_^yF0Qc>۷l]ﳦWK7ZvIL:| ժdlN39QnɌԄ/&߫mVzE@8Nf ЫlS0c ص}k`:?8Tgc\|6hdžU>Ș,#PS 85q\#u3xA21\9cԄ#f =gyxe̻;Kc1DpnMG>e|q=! QTBe t A>c#d'+8ơpm1yv2"lЛ`4.R1aٺL~djrg(a*n𺌤bJҊR:Mfvd á!.@X&W-zCM(T+ ͳk%HԤ+a#U9m s-T-pb֫$J!muR.i-VTfs(ks'^F6Q^k5ӝ_f;;xoܮj|MoMOcV|@Z/ד|20' Ԁ ܉ {є%0=q`O7qԤ/"a,;3(y+&ʑ7 h;$({v3Moו(tE[8bZQ@biF|TIANPbҿH 8$@wXuI~P|~̕eu3utϬA)w dz Y`n@x :u_.LpzsVE1_({*_*ъ]E׀` F]u"@skL±'򠴪]oDVd]55(4O" ɽ;nY<#(v$Ejhx&8u!oVxd`@>8Or G5 @G*՟vfIaZ\:@ZTǡIC3\}}~l(,CM.2X͓'C9}cF4 yE3q6<$9<.$h4OVX`S{"T+uE(!9_3SFg& H.P*-r6{UAQO~Ÿ}bڊә%10ϟX//I +ӰŐg\y \?G Yգ%Y#H >?g{Z=vlG蠘߶N&P$j >NsLJ8qT5֬K.)y)];Χ?Zn܏ۿ(X`j5}(uNٸc>_, dE]o @Cj|2{_1`UDd4n[4< !6yolpr- =@ |ʼG> o*9q3þCFV- an;bm%e G/=m~úb~/tGxlH%c#G" 9qdb,B|˨0+&$&&[e)JVpJ1!*I[wc?c6G_Uh8D`LܵZ PҿqH]5-s/0T1d|([:TQVVMo>p+ Uu[:D&@7d\ѶUS0uKK῔SGn -)3yLwm ہ^vҚR^EnkM7?K@u HWwͲut WRP~>DoUm˻D|KG%a%3ѵYBwnh|J8p-]wm&PP/xK8 Jwߵ%>}n;((w7A jo/FVu&P_U.0n6YuYp5pOR;^͠f{+,I7u_Υ7Lr# >K;Qw/u_@z灟C `t-By(? &ZZk%`3p^=ěJP÷=֮5ٺ>Q n2xJ7\0j. =============================================================================== pyinstaller – https://www.pyinstaller.org/ Copyright (c) 2010-2019, PyInstaller Development Team Copyright (c) 2005-2009, Giovanni Bajo Based on previous work under copyright (c) 2002 McMillan Enterprises, Inc. PyInstaller is licensed under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Bootloader Exception -------------------- In addition to the permissions in the GNU General Public License, the authors give you unlimited permission to link or embed compiled bootloader and related files into combinations with other programs, and to distribute those combinations without any restriction coming from the use of those files. (The General Public License restrictions do apply in other respects; for example, they cover modification of the files, and distribution when not linked into a combine executable.) =============================================================================== python – https://python.org Copyright © 2001-2019 Python Software Foundation; All Rights Reserved 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using Python 3.6.8 software in source or binary form and its associated documentation. 2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python 3.6.8 alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright © 2001-2019 Python Software Foundation; All Rights Reserved" are retained in Python 3.6.8 alone or in any derivative version prepared by Licensee. 3. In the event Licensee prepares a derivative work that is based on or incorporates Python 3.6.8 or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python 3.6.8. 4. PSF is making Python 3.6.8 available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 3.6.8 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 3.6.8 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 3.6.8, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. 7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. 8. By copying, installing or otherwise using Python 3.6.8, Licensee agrees to be bound by the terms and conditions of this License Agreement. =============================================================================== docopt – https://github.com/docopt/docopt Copyright (c) 2012 Vladimir Keleshev, Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =============================================================================== pyusb – https://github.com/pyusb/pyusb Copyright 2009–2017 Wander Lairson Costa Copyright 2009–2020 PyUSB contributors 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. =============================================================================== libusb-1.0 – https://github.com/libusb/libusb Copyright © 2001 Johannes Erdfelt Copyright © 2007-2009 Daniel Drake Copyright © 2010-2012 Peter Stuge Copyright © 2008-2016 Nathan Hjelm Copyright © 2009-2013 Pete Batard Copyright © 2009-2013 Ludovic Rousseau Copyright © 2010-2012 Michael Plante Copyright © 2011-2013 Hans de Goede Copyright © 2012-2013 Martin Pieuchot Copyright © 2012-2013 Toby Gray Copyright © 2013-2018 Chris Dickens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. The GNU Lesser General Public License, version 2.1, can be found at: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt =============================================================================== cython-hidapi – https://github.com/trezor/cython-hidapi Copyright 2011, Gary Bishop This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. =============================================================================== hidapi – https://github.com/signal11/hidapi Copyright 2009, Alan Ott This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. liquidctl-1.5.1/extra/yoda000077500000000000000000000235041401367561700155330ustar00rootroot00000000000000#!/usr/bin/env python3 """yoda – dynamically adjust liquidctl device pump and fan speeds. Periodically adjusts pump and fan speeds according to user-specified profiles. Different sensors can be used for each channel. Use show-sensors to view the sensors available for use with a particular device. To avoid jerks in pump or fan speeds, an exponential moving average is used as low-pass filter on sensor data. Usage: yoda [options] show-sensors yoda [options] control ( with on [and])... yoda --help yoda --version Options: --interval Update interval in seconds [default: 2] -m, --match Filter devices by description substring -n, --pick Pick among many results for a given filter --vendor Filter devices by vendor id --product Filter devices by product id --release Filter devices by release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus -v, --verbose Output additional information -g, --debug Show debug information on stderr --hid Override API for USB HIDs: usb, hid or hidraw --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --version Display the version number --help Show this message Examples: yoda --match grid show-sensors yoda --match grid control fan1 with '(20,20),(35,100)' on nct6793.systin yoda --match kraken show-sensors yoda --match kraken control pump with '(20,50),(50,100)' on istats.cpu and fan with '(20,25),(34,100)' on _internal.liquid Requirements: all platforms liquidctl, including the Python APIs (pip install liquidctl) Linux/FreeBSD psutil (pip install psutil) macOS iStats (gem install iStats) Windows none, system sensors not yet supported Changelog: 0.0.4 Fix casing of log and error messages 0.0.3 Remove duplicate option definition 0.0.2 Add low-pass filter and basic error handling. 0.0.1 Generalization of krakencurve-poc 0.0.2 to multiple devices. Copyright (C) 2020–2021 Jonas Malaco SPDX-License-Identifier: GPL-3.0-or-later """ import ast import logging import math import sys import time from docopt import docopt import liquidctl.cli as _borrow from liquidctl.util import normalize_profile, interpolate_profile from liquidctl.driver import * if sys.platform == 'darwin': import re import subprocess elif sys.platform.startswith('linux') or sys.platform.startswith('freebsd'): import psutil VERSION = '0.0.4' LOGGER = logging.getLogger(__name__) INTERNAL_CHIP_NAME = '_internal' MAX_FAILURES = 3 def read_sensors(device): sensors = {} for k, v, u in device.get_status(): if u == '°C': sensor_name = k.lower().replace(' ', '_').replace('_temperature', '') sensors[f'{INTERNAL_CHIP_NAME}.{sensor_name}'] = v if sys.platform == 'darwin': istats_stdout = subprocess.check_output(['istats']).decode('utf-8') for line in istats_stdout.split('\n'): if line.startswith('CPU'): cpu_temp = float(re.search(r'\d+\.\d+', line).group(0)) sensors['istats.cpu'] = cpu_temp break elif sys.platform.startswith('linux') or sys.platform.startswith('freebsd'): for m, li in psutil.sensors_temperatures().items(): for label, current, _, _ in li: sensor_name = label.lower().replace(' ', '_') sensors[f'{m}.{sensor_name}'] = current return sensors def show_sensors(device): print('{:<60} {:>18}'.format('Sensor identifier', 'Temperature')) print('-' * 80) sensors = read_sensors(device) for k, v in sensors.items(): print('{:<70} {:>6}{}'.format(k, v, '°C')) def parse_profile(arg, mintemp=0, maxtemp=100, minduty=0, maxduty=100): """Parse, validate and normalize a temperature–duty profile. >>> parse_profile('(20,30),(30,50),(34,80),(40,90)', 0, 60, 25, 100) [(20, 30), (30, 50), (34, 80), (40, 90), (60, 100)] >>> parse_profile('35', 0, 60, 25, 100) [(0, 35), (59, 35), (60, 100)] The profile is validated in structure and acceptable ranges. Duty is checked against `minduty` and `maxduty`. Temperature must be between `mintemp` and `maxtemp`. >>> parse_profile('(20,30),(50,100', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Profile must be comma-separated (temperature, duty) tuples >>> parse_profile('(20,30),(50,100,2)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Profile must be comma-separated (temperature, duty) tuples >>> parse_profile('(20,30),(50,97.6)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Duty must be integer number between 25 and 100 >>> parse_profile('(20,15),(50,100)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Duty must be integer number between 25 and 100 >>> parse_profile('(20,30),(70,100)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: Temperature must be integer number between 0 and 60 """ try: val = ast.literal_eval('[' + arg + ']') if len(val) == 1 and isinstance(val[0], int): # for arg == '' set fixed duty between mintemp and maxtemp - 1 val = [(mintemp, val[0]), (maxtemp - 1, val[0])] except: raise ValueError('profile must be comma-separated (temperature, duty) tuples') for step in val: if not isinstance(step, tuple) or len(step) != 2: raise ValueError('profile must be comma-separated (temperature, duty) tuples') temp, duty = step if not isinstance(temp, int) or temp < mintemp or temp > maxtemp: raise ValueError('temperature must be integer between {} and {}'.format(mintemp, maxtemp)) if not isinstance(duty, int) or duty < minduty or duty > maxduty: raise ValueError('duty must be integer between {} and {}'.format(minduty, maxduty)) return normalize_profile(val, critx=maxtemp) def control(device, channels, profiles, sensors, update_interval): LOGGER.info('device: %s on bus %s and address %s', device.description, device.bus, device.address) for channel, profile, sensor in zip(channels, profiles, sensors): LOGGER.info('channel: %s following profile %s on %s', channel, str(profile), sensor) averages = [None] * len(channels) cutoff_freq = 1 / update_interval / 10 alpha = 1 - math.exp(-2 * math.pi * cutoff_freq) LOGGER.info('update interval: %d s; cutoff frequency (low-pass): %.2f Hz; ema alpha: %.2f', update_interval, cutoff_freq, alpha) try: # more efficient and safer API, but only supported by very few devices apply_duty = device.set_instantaneous_speed except AttributeError: apply_duty = device.set_fixed_speed LOGGER.info('starting...') failures = 0 while True: try: sensor_data = read_sensors(device) for i, (channel, profile, sensor) in enumerate(zip(channels, profiles, sensors)): # compute the exponential moving average (ema), used as a low-pass filter (lpf) ema = averages[i] sample = sensor_data[sensor] if ema is None: ema = sample else: ema = alpha * sample + (1 - alpha) * ema averages[i] = ema # interpolate on sensor ema and apply corresponding duty duty = interpolate_profile(profile, ema) LOGGER.info('%s control: lpf(%s) = lpf(%.1f°C) = %.1f°C => duty := %d%%', channel, sensor, sample, ema, duty) apply_duty(channel, duty) failures = 0 except Exception as err: failures += 1 LOGGER.error(err) if failures >= MAX_FAILURES: LOGGER.critical('too many failures in a row: %d', failures) raise time.sleep(update_interval) if __name__ == '__main__': if len(sys.argv) == 2 and sys.argv[1] == 'doctest': import doctest doctest.testmod(verbose=True) sys.exit(0) args = docopt(__doc__, version='yoda v{}'.format(VERSION)) if args['--debug']: args['--verbose'] = True logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(name)s: %(message)s') import liquidctl.version LOGGER.debug('yoda v%s', VERSION) LOGGER.debug('liquidctl v%s', liquidctl.version.__version__) elif args['--verbose']: logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s') LOGGER.setLevel(logging.INFO) else: logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s') sys.tracebacklimit = 0 frwd = _borrow._make_opts(args) selected = list(find_liquidctl_devices(**frwd)) if len(selected) > 1: raise SystemExit('too many devices, filter or select one. See liquidctl --help and yoda --help.') elif len(selected) == 0: raise SystemExit('no devices matches available drivers and selection criteria') device = selected[0] device.connect() try: if args['show-sensors']: show_sensors(device) elif args['control']: control(device, args[''], list(map(parse_profile, args[''])), args[''], update_interval=int(args['--interval'])) else: raise Exception('nothing to do') except KeyboardInterrupt: LOGGER.info('stopped by user.') finally: device.disconnect() liquidctl-1.5.1/liquidctl.8000066400000000000000000000321611401367561700156100ustar00rootroot00000000000000'\" t .nr is_macos 0 .TH LIQUIDCTL 8 2021\-01\-25 "liquidctl" "System Manager's Manual" . .SH NAME liquidctl \- monitor and control liquid coolers and other devices . .SH SYNOPSIS .SY liquidctl .RI [ options ] .B list .SY liquidctl .RI [ options ] .B initialize .RB [ all ] .SY liquidctl .RI [ options ] .B status .SY liquidctl .RI [ options ] .B set .I channel .B speed .RI ( temperature .IR percentage ) \&.\|.\|.\& .SY liquidctl .RI [ options ] .B set .I channel .B speed .I percentage .SY liquidctl .RI [ options ] .B set .I channel .B color .I mode .RI [ color \&.\|.\|.\&] .SY liquidctl .B \-\-version .SY liquidctl .B \-\-help .YS . .SH DESCRIPTION \fBliquidctl\fR is a utility for overseeing and controlling some hardware monitoring devices not yet supported at the kernel level. .if !\n[is_macos]\{ Because \fBliquidctl\fR directly accesses the hardware devices, root privileges are generally required, though this can be avoided with appropriate udev rules. .\} .PP \fBliquidctl list\fR outputs all compatible devices found on the system. In case more than one device is found, the desired one can be selected for later invocations with \fB--match=\fIsubstring\fR, where \fIsubstring\fR matches part of the desired device's description using a case insensitive comparison. .PP \fBliquidctl list \fI\-\-verbose\fR enables the display of additional identifiers and addresses that can also be used to select specific devices. These can be better suited for certain use cases, such as non-interactive scripts. .PP \fBliquidctl initialize\fR prepares a device for later commands, and most devices must be initialized after every boot or when resuming from a suspended state. Unless finer control is required, all devices can be initialized at once with \fBliquidctl initialize all\fR. Some devices may output some information at this stage. .PP \fBliquidctl status\fR displays the status of all devices that match the provided filtering options. .PP \fBliquidctl set \fIchannel\fB speed\fR allows the user to set fan and pump speeds. These, depending on the device, can be set to fixed duty values, variable temperature\–duty curves, or both. .PP \fBliquidctl set \fIchannel\fB color\fR allows the user to configure and set lighting modes. Supported lighting modes and additional options vary by device and are listed in later sections of this manual. Each color can be specified as: .IP \(bu hexadecimal RGB with or without prefix '0x': \fIff7f3f\fR; .IP \(bu decimal RGB triple, R,G,B ∊ [0, 255]: \fIrgb(255,127,63)\fR; .IP \(bu hue\-saturation\-value HSV triple, H ∊ [0, 360], S,V ∊ [0, 100]: \fIhsv(20,75,100)\fR; .IP \(bu hue\-saturation\-lightness HSL triple, H ∊ [0, 360], S,L ∊ [0, 100]: \fIhsl(20,100,62)\fR. . .SH OPTIONS . .SS Device selection options Devices can be selected using one or more values taken from \fBlist \fI\-\-verbose\fP. .TP .BI \-m\ substring\fR,\ \fP \-\-match= substring Filter devices by case insensitive substring of device description. .TP .BI \-n\ number\fR,\ \fP \-\-pick= number Pick among many results for a given filter. .TP .BI \-\-vendor= id Filter devices by hexadecimal vendor id. .TP .BI \-\-product= id Filter devices by hexadecimal product id. .TP .BI \-\-release= number Filter devices by hexadecimal release number. .TP .BI \-\-serial= number Filter devices by serial number. .TP .BI \-\-bus= bus Filter devices by bus. .TP .BI \-\-address= address Filter devices by address in bus. .TP .BI \-\-usb\-port= port Filter devices by USB port in bus. .TP .BI \-d\ id\fR,\ \fP \-\-device= id Select device by listing ID. . .SS Animation options Some devices and animation modes support additional options. .TP .BI \-\-speed= value Abstract animation speed (device/mode specific). .TP .BI \-\-time\-per\-color= value Time to wait on each color (seconds). .TP .BI \-\-time\-off= value Time to wait with the LED turned off (seconds). .TP .BI \-\-alert\-threshold= number Threshold temperature for a visual alert (degrees Celsius). .TP .BI \-\-alert\-color= color Color used by the visual high temperature alert. .TP .BI \-\-direction= string If the pattern should move forward or backwards. [default: forward]. .TP .BI \-\-start\-led= number The first led to start the effect at. .TP .BI \-\-maximum\-leds= number The number of LED's the effect should apply to. . .SS Other options .TP .B \-\-single\-12v\-ocp Enable single rail +12V OCP. .TP .B \-\-pump\-mode= mode Set the pump mode. .TP .B \-\-temperature\-sensor= number The temperature sensor number for the Commander Pro. .TP .B \-\-legacy\-690lc Use Asetek 690LC in legacy mode (old Krakens). .TP .B \-\-non\-volatile Store on non\-volatile controller memory. .TP .B \-\-unsafe= features Comman-separated bleeding-edge features to enable. .TP .B \-v\fR, \fP\-\-verbose Output additional information. .TP .B \-g\fR, \fB\-\-debug Show debug information on \fIstderr\fR. .TP .B \-\-version Display the version number. .TP .B \-\-help Show the embedded help. . .SH EXIT STATUS 1 if there was an error, 0 otherwise. . .SH FILES .TP .ie \n[is_macos] .I ~/Library/Caches/liquidctl/* .el .IR $XDG_RUNTIME_DIR/liquidctl/* ,\ /var/run/liquidctl/* ,\ /tmp/liquidctl/* Internal data used by some drivers. .\" e.g. RuntimeStorage for Legacy690Lc and HydroPlatinum . .SH EXAMPLE .SY liquidctl .B list \-\-verbose .SY liquidctl .B initialize all .SY liquidctl .BI \-\-match\ kraken\ set\ pump\ speed\ 90 .SY liquidctl .BI \-\-product\ 170e\ set\ led\ color\ fading .I 350017 ff2608 .SY liquidctl .B status .YS . .SH DEVICE SPECIFICS . .SS Corsair Commander Pro .SS Corsair Lighting Node Pro Cooling channels: \fIsync\fR, \fIfan[1\-5]\fR. (Commander Pro only) .PP Lighting channels: \fIled1\fR, \fIled2\fR. .TS l c --- l c . Mode #colors \fIclear\fR 0 \fIoff\fR 0 \fIfixed\fR 1 \fIcolor_shift\fR 2 \fIcolor_pulse\fR 2 \fIcolor_wave\fR 2 \fIvisor\fR 2 \fIblink\fR 2 \fImarquee\fR 1 \fIsequential\fR 1 \fIrainbow\fR 0 \fIrainbow2\fR 0 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIfast\fR, \fImedium\fR, \fIslow\fR. .PP The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. .BI \-\-start\-led= number , the first LED that the lighting effect should be for. .BI \-\-start\-led= number , the first LED that the lighting effect should be for. .BI \-\-maximum\-led= number , the number of LEDs that the lighting effect should applied to. .BI \-\-temperature\-sensor= number , The temperature sensor that should be used to control the fan curves, probe 1 by default. .SS Corsair H80i GT, H100i GTX, H110i GTX .SS Corsair H80i v2, H100i v2, H115i .SS EVGA CLC 120 (CL12), 240, 280, 360 Cooling channels: \fIpump\fR, \fIfan\fR. .PP Lighting channels: \fIlogo\fR. .TS l c c --- l c c . Mode #colors notes \fIrainbow\fR 0 only availble on EVGA coolers \fIfading\fR 2 \fIblinking\fR 1 \fIfixed\fR 1 \fIblackout\fR 0 no high-temperature alerts .TE .PP The \fIrainbow\fR mode speed can be configured with .BI \-\-speed= [1\(en6] . The speed of the other modes is instead customized with .B \-\-time\-per\-color .RI ( fading\ and\ blinking ) and .B \-\-time\-off .RI ( blinking\ only). .PP All modes except .I blackout support a visual high-temperature alert configured with .B \-\-alert\-threshold and .BR \-\-alert\-color . . .SS Corsair H100i Platinum, H100i Platinum SE, H115i Platinum .SS Corsair H100i Pro XT, H115i Pro XT Fan channels: \fIfan\fR, \fIfan[1\(en2]\fR. .PP Pump mode (\fBinitialize \-\-pump\-mode \fImode\fR): \fIquiet\fR, \fIbalanced\fR (default), \fIextreme\fR. .PP Lighting channels: \fIsync\fR, \fIled\fR. .TS l l c c c ----- l l c c c . Channel Mode #colors (Platinum) #colors (Pro XT) #colors (Platinum SE) \fIled\fR \fIoff\fR 0 0 0 \fIled\fR \fIfixed\fR 1 1 1 \fIled\fR \fIsuper\-fixed\fR 24 16 48 .TE . .SS NZXT Kraken X40, X60 .SS NZXT Kraken X31, X41, X61 Supports the same modes and options as a Corsair H80i GT (or similar), but requires \fB\-\-legacy\-690lc\fR to be passed on all invocations. . .SS NZXT Kraken M22 .SS NZXT Kraken X42, X52, X62, X72 Cooling channels (only X42, X52, X62, X72): \fIpump\fR, \fIfan\fR. .PP Lighting channels: \fIlogo\fR, \fIring\fR, \fIsync\fR. .TS l c c c ---- l c c c . Mode logo ring #colors \fIoff\fR yes yes 0 \fIfixed\fR yes yes 1 \fIsuper\-fixed\fR yes yes 1\(en9 \fIfading\fR yes yes 2\(en8 \fIalternating\fR no yes 2 \fIbreathing\fR yes yes 1\(en8 \fIsuper\-breathing\fR yes yes 1\(en9 \fIpulse\fR yes yes 1\(en8 \fItai\-chi\fR no yes 2 \fIwater\-cooler\fR no yes 0 \fIloading\fR no yes 1 \fIwings\fR no yes 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. . .SS NZXT Kraken X53, X63, X73 .SS NZXT Kraken Z63, Z73 Cooling channels: \fIpump\fR; (only Z63, Z73:) \fIfan\fR. .PP Lighting channels: \fIexternal\fR; (only X53, X63, X73:) \fIring\fR, \fIlogo\fR, \fIsync\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIfading\fR 2\(en8 \fIsuper\-fixed\fR 1\(en40 \fIalternating\-[3\-6]\fR 1\(en2 \fIpulse\fR 1\(en8 \fIbreathing\fR 1\(en8 \fIsuper\-breathing\fR 1\(en40 \fIcandle\fR 1 \fIstarry\-night\fR 1 \fIloading\fR 1 \fItai\-chi\fR 1\(en2 \fIwater\-cooler\fR 2 \fIwings\fR 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. . .SS Corsair HX750i, HX850i, HX1000i, HX1200i .SS Corsair RM650i, RM750i, RM850i, RM1000i Fan channels: \fIfan\fR. .PP Lighting channels: none. .PP Setting a fixed fan speed changes the fan mode to software control. To revert back to hardware control, run \fBinitialize\fR again. .PP (Experimental feature) The +12V rails normally function in multiple-rail mode. Single-rail mode can be selected by passing \fB\-\-single\-12v\-ocp\fR to \fBinitialize\fR. To revert back to multiple-rail mode, run \fBinitialize\fR again without that flag. . .SS NZXT E500, E650, E850 Fan channels: none (feature not supported yet). .PP Lighting channels: none. . .SS NZXT Grid+ V3 Fan channels: \fIfan[1\(en6]\fR, \fIsync\fR. .PP Lighting channels: none. . .SS NZXT Smart Device (V1) Fan channels: \fIfan[1\(en3]\fR, \fIsync\fR. .PP Lighting channels: \fIled\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIsuper\-fixed\fR 1\(en40 \fIfading\fR 2\(en8 \fIalternating\fR 2 \fIbreathing\fR 1\(en8 \fIsuper\-breathing\fR 1\(en40 \fIpulse\fR 1\(en8 \fIcandle\fR 1 \fIwings\fR 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. . .SS NZXT Smart Device V2 .SS NZXT RGB & Fan Controller .SS NZXT HUE 2 .SS NZXT HUE 2 Ambient Fan channels (only Smart Device V2 and RGB & Fan Controller): \fIfan[1\(en3]\fR. .PP Lighting channels: \fIled[1\(en2]\fR, \fIsync\fR. .PP Additional lighting channels (only HUE 2): \fIled[3\(en4]\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIsuper\-fixed\fR 1\(en40 \fIfading\fR 2\(en8 \fIalternating\-[3\-6]\fR 2 \fIpulse\fR 1\(en8 \fIbreathing\fR 1\(en8 \fIsuper\-breathing\fR 1\(en40 \fIcandle\fR 1 \fIstarry\-night\fR 1 \fIwings\fR 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. . .SS ASUS Strix RTX 2080 Ti OC Fan channels: none. .PP Lighting channels: \fIled\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIflash\fR 1 \fIbreathing\fR 1 \fIrainbow\fR 0 .TE . .SS Corsair Vengeance RGB Fan channels: none. .PP Lighting channels: \fIled\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIbreathing\fR 1\(en7 \fIfading\fR 2\(en7 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. . .SS Gigabyte RGB Fusion 2.0 5702 Controller .SS Gigabyte RGB Fusion 2.0 8297 Controller Fan channels: none. .PP Lighting channels: \fIled[1\(en8]\fR, \fIsync\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIpulse\fR 1 \fI(double\-)?flash\fR 1 \fIcolor\-cycle\fR 0 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR, \fIludicrous\fR. . .SS EVGA GTX 1080 FTW Fan channels: none. .PP Lighting channels: \fIled\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIbreathing\fR 1 \fIrainbow\fR 0 .TE . .SH SEE ALSO The complete documentation is available in the project's sources and .UR https://github.com/liquidctl/liquidctl homepage .UE . liquidctl-1.5.1/liquidctl/000077500000000000000000000000001401367561700155145ustar00rootroot00000000000000liquidctl-1.5.1/liquidctl/__init__.py000066400000000000000000000001221401367561700176200ustar00rootroot00000000000000from liquidctl.driver import find_liquidctl_devices from liquidctl.error import * liquidctl-1.5.1/liquidctl/cli.py000066400000000000000000000326401401367561700166420ustar00rootroot00000000000000"""liquidctl – monitor and control liquid coolers and other devices. Usage: liquidctl [options] list liquidctl [options] initialize [all] liquidctl [options] status liquidctl [options] set speed ( ) ... liquidctl [options] set speed liquidctl [options] set color [] ... liquidctl --help liquidctl --version Device selection options (see: list -v): -m, --match Filter devices by description substring -n, --pick Pick among many results for a given filter --vendor Filter devices by hexadecimal vendor ID --product Filter devices by hexadecimal product ID --release Filter devices by hexadecimal release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus -d, --device Select device by listing id Animation options (devices/modes can support zero or more): --speed Abstract animation speed (device/mode specific) --time-per-color Time to wait on each color (seconds) --time-off Time to wait with the LED turned off (seconds) --alert-threshold Threshold temperature for a visual alert (°C) --alert-color Color used by the visual high temperature alert --direction If the pattern should move forward or backwards. [default: forward] --start-led The first led to start the effect at --maximum-leds The number of LED's the effect should apply to Other device options: --single-12v-ocp Enable single rail +12V OCP --pump-mode Set the pump mode (certain Corsair coolers) --temperature-sensor The temperature sensor number for the Commander Pro --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --non-volatile Store on non-volatile controller memory --unsafe Comma-separated bleeding-edge features to enable Other interface options: -v, --verbose Output additional information -g, --debug Show debug information on stderr --version Display the version number --help Show this message Deprecated: --hid Deprecated Copyright (C) 2018–2021 Jonas Malaco, Marshall Asch, CaseySJ, Tom Frey and contributors liquidctl incorporates work by leaty, Ksenija Stanojevic, Alexander Tong, Jens Neumaier, Kristóf Jakab, Sean Nelson, Chris Griffith, notaz, realies and Thomas Pircher. SPDX-License-Identifier: GPL-3.0-or-later This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. """ import datetime import errno import inspect import logging import os import sys from traceback import format_exception from docopt import docopt from liquidctl.driver import * from liquidctl.error import NotSupportedByDevice, NotSupportedByDriver, UnsafeFeaturesNotEnabled from liquidctl.util import color_from_str from liquidctl.version import __version__ # conversion from CLI arg to internal option; as options as forwarded to bused # and drivers, they must: # - have no default value in the CLI level (not forwarded unless explicitly set); # - and avoid unintentional conflicts with target function arguments _PARSE_ARG = { '--vendor': lambda x: int(x, 16), '--product': lambda x: int(x, 16), '--release': lambda x: int(x, 16), '--serial': str, '--bus': str, '--address': str, '--usb-port': lambda x: tuple(map(int, x.split('.'))), '--match': str, '--pick': int, '--speed': str, '--time-per-color': int, '--time-off': int, '--alert-threshold': int, '--alert-color': color_from_str, '--temperature-sensor': int, '--direction': str, '--start-led': int, '--maximum-leds': int, '--single-12v-ocp': bool, '--pump-mode': str, '--legacy-690lc': bool, '--non-volatile': bool, '--unsafe': lambda x: x.lower().split(','), '--verbose': bool, '--debug': bool, } # options that cause liquidctl.driver.find_liquidctl_devices to ommit devices _FILTER_OPTIONS = [ 'vendor', 'product', 'release', 'serial', 'bus', 'address', 'usb-port', 'match', 'pick', # --device generates no option ] # custom number formats for values of select units _VALUE_FORMATS = { '°C': '.1f', 'rpm': '.0f', 'V': '.2f', 'A': '.2f', 'W': '.2f' } _LOGGER = logging.getLogger(__name__) def _list_devices(devices, using_filters=False, device_id=None, verbose=False, debug=False, **opts): for i, dev in enumerate(devices): warnings = [] if not using_filters: print(f'Device ID {i}: {dev.description}') elif device_id is not None: print(f'Device ID {device_id}: {dev.description}') else: print(f'Result #{i}: {dev.description}') if not verbose: continue if dev.vendor_id: print(f'├── Vendor ID: {dev.vendor_id:#06x}') if dev.product_id: print(f'├── Product ID: {dev.product_id:#06x}') if dev.release_number: print(f'├── Release number: {dev.release_number:#06x}') try: if dev.serial_number: print(f'├── Serial number: {dev.serial_number}') except: msg = 'could not read the serial number' if sys.platform.startswith('linux') and os.geteuid: msg += ' (requires root privileges)' elif sys.platform in ['win32', 'cygwin'] and 'Hid' not in type(dev.device).__name__: msg += ' (device possibly requires a kernel driver)' if debug: _LOGGER.exception(msg.capitalize()) else: warnings.append(msg) print(f'├── Bus: {dev.bus}') print(f'├── Address: {dev.address}') if dev.port: port = '.'.join(map(str, dev.port)) print(f'├── Port: {port}') print(f'└── Driver: {type(dev).__name__}') if debug: driver_hier = [i.__name__ for i in inspect.getmro(type(dev)) if i != object] _LOGGER.debug('hierarchy: %s', ', '.join(driver_hier[1:])) for msg in warnings: _LOGGER.warning(msg) print('') assert 'device' not in opts or len(devices) <= 1, 'too many results listed with --device' def _print_dev_status(dev, status): if not status: return print(dev.description) tmp = [] kcols, vcols = 0, 0 for k, v, u in status: if isinstance(v, datetime.timedelta): v = str(v) u = '' else: valfmt = _VALUE_FORMATS.get(u, '') v = f'{v:{valfmt}}' kcols = max(kcols, len(k)) vcols = max(vcols, len(v)) tmp.append((k, v, u)) for k, v, u in tmp[:-1]: print(f'├── {k:<{kcols}} {v:>{vcols}} {u}') k, v, u = tmp[-1] print(f'└── {k:<{kcols}} {v:>{vcols}} {u}') print('') def _device_set_color(dev, args, **opts): color = map(color_from_str, args['']) dev.set_color(args[''], args[''], color, **opts) def _device_set_speed(dev, args, **opts): if len(args['']) > 0: profile = zip(map(int, args['']), map(int, args[''])) dev.set_speed_profile(args[''], profile, **opts) else: dev.set_fixed_speed(args[''], int(args[''][0]), **opts) def _make_opts(args): if args['--hid']: _LOGGER.warning('ignoring --hid %s: deprecated option, API will be selected automatically', args['--hid']) opts = {} for arg, val in args.items(): if val is not None and arg in _PARSE_ARG: opt = arg.replace('--', '').replace('-', '_') opts[opt] = _PARSE_ARG[arg](val) return opts def _gen_version(): extra = None try: from liquidctl.extraversion import __extraversion__ if __extraversion__['editable']: extra = ['editable'] elif __extraversion__['dist_name'] and __extraversion__['dist_package']: extra = [__extraversion__['dist_name'], __extraversion__['dist_package']] else: extra = [__extraversion__['commit'][:12]] if __extraversion__['dirty']: extra[0] += '-dirty' except: return f'liquidctl v{__version__}' return f'liquidctl v{__version__} ({"; ".join(extra)})' def main(): args = docopt(__doc__) if args['--version']: print(_gen_version()) sys.exit(0) if args['--debug']: args['--verbose'] = True logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(name)s: %(message)s') _LOGGER.debug('running %s', _gen_version()) elif args['--verbose']: logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') else: logging.basicConfig(level=logging.WARNING, format='%(levelname)s: %(message)s') sys.tracebacklimit = 0 opts = _make_opts(args) filter_count = sum(1 for opt in opts if opt in _FILTER_OPTIONS) device_id = None if not args['--device']: selected = list(find_liquidctl_devices(**opts)) else: device_id = int(args['--device']) no_filters = {opt: val for opt, val in opts.items() if opt not in _FILTER_OPTIONS} compat = list(find_liquidctl_devices(**no_filters)) if device_id < 0 or device_id >= len(compat): raise SystemExit('Error: device ID out of bounds') if filter_count: # check that --device matches other filter criteria matched_devs = [dev.device for dev in find_liquidctl_devices(**opts)] if compat[device_id].device not in matched_devs: raise SystemExit('Error: device ID does not match remaining selection criteria') _LOGGER.warning('mixing --device with other filters is not recommended; ' 'to disambiguate between results prefer --pick ') selected = [compat[device_id]] if args['list']: _list_devices(selected, using_filters=bool(filter_count), device_id=device_id, **opts) return if len(selected) > 1 and not (args['status'] or args['all']): raise SystemExit('Error: too many devices, filter or select one (see: liquidctl --help)') elif len(selected) == 0: raise SystemExit('Error: no devices matches available drivers and selection criteria') errors = 0 def log_error(err, msg, append_err=False, *args): nonlocal errors errors += 1 _LOGGER.info('%s', err, exc_info=True) if append_err: exception = list(format_exception(Exception, err, None))[-1].rstrip() _LOGGER.error(f'{msg}: {exception}', *args) else: _LOGGER.error(msg, *args) for dev in selected: _LOGGER.debug('device: %s', dev.description) try: with dev.connect(**opts): if args['initialize']: _print_dev_status(dev, dev.initialize(**opts)) elif args['status']: _print_dev_status(dev, dev.get_status(**opts)) elif args['set'] and args['speed']: _device_set_speed(dev, args, **opts) elif args['set'] and args['color']: _device_set_color(dev, args, **opts) else: assert False, 'unreachable' except OSError as err: # each backend API returns a different subtype of OSError (OSError, # usb.core.USBError or PermissionError) for permission issues if err.errno in [errno.EACCES, errno.EPERM]: log_error(err, f'Error: insufficient permissions to access {dev.description}') elif err.args == ('open failed', ): log_error(err, f'Error: could not open {dev.description}, possibly due to insufficient permissions') else: log_error(err, f'Unexpected OS error with {dev.description}', append_err=True) except NotSupportedByDevice as err: log_error(err, f'Error: operation not supported by {dev.description}') except NotSupportedByDriver as err: log_error(err, f'Error: operation not supported by driver for {dev.description}') except UnsafeFeaturesNotEnabled as err: features = ','.join(err.args) log_error(err, f'Error: missing --unsafe features for {dev.description}: {features!r}') _LOGGER.error('More information is provided in the corresponding device guide') except Exception as err: log_error(err, f'Unexpected error with {dev.description}', append_err=True) if errors: sys.exit(errors) def find_all_supported_devices(**opts): """Deprecated.""" _LOGGER.warning('deprecated: use liquidctl.driver.find_liquidctl_devices instead') return find_liquidctl_devices(**opts) if __name__ == '__main__': main() liquidctl-1.5.1/liquidctl/driver/000077500000000000000000000000001401367561700170075ustar00rootroot00000000000000liquidctl-1.5.1/liquidctl/driver/__init__.py000066400000000000000000000047331401367561700211270ustar00rootroot00000000000000"""Drivers and buses package for liquidctl. The typical use case of generic scripts and interfaces – including the liquidctl CLI – is to instantiate drivers for all known devices found on the system. from liquidctl.driver import * for dev in find_liquidctl_devices(): print(dev.description) Is also possible to find devices compatible with a specific driver. from liquidctl.driver.kraken_two import KrakenTwoDriver for dev in KrakenTwoDriver.find_supported_devices(): print(dev.description) Copyright (C) 2018–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import sys from liquidctl.driver.base import BaseBus, find_all_subclasses from liquidctl.driver import asetek from liquidctl.driver import corsair_hid_psu from liquidctl.driver import hydro_platinum from liquidctl.driver import commander_pro from liquidctl.driver import kraken2 from liquidctl.driver import kraken3 from liquidctl.driver import nzxt_epsu from liquidctl.driver import rgb_fusion2 from liquidctl.driver import smart_device if sys.platform == 'linux': from liquidctl.driver import ddr4 from liquidctl.driver import nvidia def find_liquidctl_devices(pick=None, **kwargs): """Find devices and instantiate corresponding liquidctl drivers. Probes all buses and drivers that have been loaded at the time of the call and yields driver instances. Filter conditions can be passed through to the buses and drivers via `**kwargs`. A driver instance will be yielded for each compatible device that matches the supplied filter conditions. If `pick` is passed, only the driver instance for the `(pick + 1)`-th matched device will be yielded. """ buses = sorted(find_all_subclasses(BaseBus), key=lambda x: (x.__module__, x.__name__)) num = 0 for bus_cls in buses: for dev in bus_cls().find_devices(**kwargs): if pick is not None: if num == pick: yield dev return num += 1 else: yield dev __all__ = [ 'find_liquidctl_devices', ] # allow old driver imports to continue to work by manually placing these into # the module cache, so import liquidctl.driver.foo does not need to # check the filesystem for foo sys.modules['liquidctl.driver.kraken_two'] = kraken2 sys.modules['liquidctl.driver.nzxt_smart_device'] = smart_device sys.modules['liquidctl.driver.seasonic'] = nzxt_epsu liquidctl-1.5.1/liquidctl/driver/asetek.py000066400000000000000000000431051401367561700206400ustar00rootroot00000000000000"""liquidctl drivers for fifth generation Asetek 690LC liquid coolers. Supported devices: - EVGA CLC (120 CL12, 240, 280 or 360); modern generic Asetek 690LC - NZXT Kraken X (X31, X41 or X61); legacy generic Asetek 690LC - NZXT Kraken X (X40 or X60); legacy generic Asetek 690LC - Corsair H80i GT, H100i GTX or H110i GTX - Corsair H80i v2, H100i v2 or H115i Copyright (C) 2018–2021 Jonas Malaco and contributors Incorporates or uses as reference work by Kristóf Jakab, Sean Nelson and Chris Griffith. SPDX-License-Identifier: GPL-3.0-or-later """ import logging import usb from liquidctl.driver.usb import UsbDriver from liquidctl.error import NotSupportedByDevice from liquidctl.keyval import RuntimeStorage from liquidctl.util import clamp _LOGGER = logging.getLogger(__name__) _CMD_RUNTIME = 0x10 _CMD_PROFILE = 0x11 _CMD_OVERRIDE = 0x12 _CMD_PUMP_PWM = 0x13 _CMD_LUID = 0x14 _CMD_READ_ONLY_RUNTIME = 0x20 _CMD_STORE_SETTINGS = 0x21 _CMD_EXTERNAL_TEMPERATURE = 0x22 _FIXED_SPEED_CHANNELS = { # (message type, minimum duty, maximum duty) 'pump': (_CMD_PUMP_PWM, 50, 100), # min/max must correspond to _MIN/MAX_PUMP_SPEED_CODE } _VARIABLE_SPEED_CHANNELS = { # (message type, minimum duty, maximum duty) 'fan': (_CMD_PROFILE, 0, 100) } _MAX_PROFILE_POINTS = 6 _CRITICAL_TEMPERATURE = 60 _HIGH_TEMPERATURE = 45 _MIN_PUMP_SPEED_CODE = 0x32 _MAX_PUMP_SPEED_CODE = 0x42 _READ_ENDPOINT = 0x82 _READ_LENGTH = 32 _READ_TIMEOUT = 2000 _WRITE_ENDPOINT = 0x2 _WRITE_TIMEOUT = 2000 _LEGACY_FIXED_SPEED_CHANNELS = { # (message type, minimum duty, maximum duty) 'fan': (_CMD_OVERRIDE, 0, 100), 'pump': (_CMD_PUMP_PWM, 50, 100), } # USBXpress specific control parameters; from the USBXpress SDK # (Customization/CP21xx_Customization/AN721SW_Linux/silabs_usb.h) _USBXPRESS_REQUEST = 0x02 _USBXPRESS_FLUSH_BUFFERS = 0x01 _USBXPRESS_CLEAR_TO_SEND = 0x02 _USBXPRESS_NOT_CLEAR_TO_SEND = 0x04 _USBXPRESS_GET_PART_NUM = 0x08 # Unknown control parameters; from Craig's libSiUSBXp and OpenCorsairLink _UNKNOWN_OPEN_REQUEST = 0x00 _UNKNOWN_OPEN_VALUE = 0xffff # Control request type _USBXPRESS = usb.util.CTRL_OUT | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE class _CommonAsetekDriver(UsbDriver): """Common methods for Asetek 690LC devices.""" def _configure_flow_control(self, clear_to_send): """Set the software clear-to-send flow control policy for device.""" _LOGGER.debug('set clear to send = %s', clear_to_send) if clear_to_send: self.device.ctrl_transfer(_USBXPRESS, _USBXPRESS_REQUEST, _USBXPRESS_CLEAR_TO_SEND) else: self.device.ctrl_transfer(_USBXPRESS, _USBXPRESS_REQUEST, _USBXPRESS_NOT_CLEAR_TO_SEND) def _begin_transaction(self): """Begin a new transaction before writing to the device.""" _LOGGER.debug('begin transaction') self.device.claim() self.device.ctrl_transfer(_USBXPRESS, _USBXPRESS_REQUEST, _USBXPRESS_FLUSH_BUFFERS) def _write(self, data): self.device.write(_WRITE_ENDPOINT, data, _WRITE_TIMEOUT) def _end_transaction_and_read(self): """End the transaction by reading from the device. According to the official documentation, as well as Craig's open-source implementation (libSiUSBXp), it should be necessary to check the queue size and read the data in chunks. However, leviathan and its derivatives seem to work fine without this complexity; we also successfully follow this approach. """ msg = self.device.read(_READ_ENDPOINT, _READ_LENGTH, _READ_TIMEOUT) self.device.release() return msg def _configure_device(self, color1=[0, 0, 0], color2=[0, 0, 0], color3=[255, 0, 0], alert_temp=_HIGH_TEMPERATURE, interval1=0, interval2=0, blackout=False, fading=False, blinking=False, enable_alert=True): self._write([0x10] + color1 + color2 + color3 + [alert_temp, interval1, interval2, not blackout, fading, blinking, enable_alert, 0x00, 0x01]) def _prepare_profile(self, profile, min_duty, max_duty): opt = list(profile) size = len(opt) if size < 1: raise ValueError('at least one PWM point required') elif size > _MAX_PROFILE_POINTS: raise ValueError(f'too many PWM points ({size}), only 6 supported') for i, (temp, duty) in enumerate(opt): opt[i] = (temp, clamp(duty, min_duty, max_duty)) missing = _MAX_PROFILE_POINTS - size if missing: # Some issues were observed when padding with (0°C, 0%), though # they were hard to reproduce. So far it *seems* that in some # instances the device will store the last "valid" profile index # somewhere, and would need another call to initialize() to clear # that up. Padding with (CRIT, 100%) appears to avoid all issues, # at least within the reasonable range of operating temperatures. _LOGGER.info('filling missing %d PWM points with (60°C, 100%%)', missing) opt = opt + [(_CRITICAL_TEMPERATURE, 100)]*missing return opt def connect(self, **kwargs): """Connect to the device. Enables the device to send data to the host. """ ret = super().connect(**kwargs) self._configure_flow_control(clear_to_send=True) return ret def initialize(self, **kwargs): """Initialize the device.""" self._begin_transaction() self._configure_device() self._end_transaction_and_read() def disconnect(self, **kwargs): """Disconnect from the device. Implementation note: unlike SI_Close is supposed to do,¹ do not send _USBXPRESS_NOT_CLEAR_TO_SEND to the device. This allows one program to disconnect without stopping reads from another. Surrounding device.read() with _USBXPRESS_[NOT_]CLEAR_TO_SEND would make more sense, but there seems to be a yet unknown minimum delay necessary for that to work reliably. ¹ https://github.com/craigshelley/SiUSBXp/blob/master/SiUSBXp.c """ super().disconnect(**kwargs) class Modern690Lc(_CommonAsetekDriver): """Modern fifth generation Asetek 690LC cooler.""" SUPPORTED_DEVICES = [ (0x2433, 0xb200, None, 'Asetek 690LC (assuming EVGA CLC)', {}), ] @classmethod def probe(cls, handle, legacy_690lc=False, **kwargs): if legacy_690lc: return yield from super().probe(handle, **kwargs) def downgrade_to_legacy(self): """Take the device handle and return a new Legacy690Lc instance for it. This method returns a new instance that takes the device handle from `self`. Because of this, the caller should immediately discard `self`, as it is no longer valid to call any of its methods or access any of its properties. While it is sometimes possible to downgrade a device that has seen modern traffic since it booted, this will generally not work. Additionally, no attempt to disconnect from the device is made while downgrading the instance. Thus, callers are strongly advised to only call this function before connecting to the device from this instance and, in fact, before calling any other methods at all on the device, from any instance. Finally, this method is not yet considered stable and its signature and/or behavior may change. Callers should follow the development of liquidctl and the stabilization of this API. """ legacy = Legacy690Lc(self.device, self._description) self.device = None self._description = None return legacy def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ self._begin_transaction() self._write([_CMD_LUID, 0, 0, 0]) msg = self._end_transaction_and_read() firmware = '{}.{}.{}.{}'.format(*tuple(msg[0x17:0x1b])) return [ ('Liquid temperature', msg[10] + msg[14]/10, '°C'), ('Fan speed', msg[0] << 8 | msg[1], 'rpm'), ('Pump speed', msg[8] << 8 | msg[9], 'rpm'), ('Firmware version', firmware, '') ] def set_color(self, channel, mode, colors, time_per_color=1, time_off=None, alert_threshold=_HIGH_TEMPERATURE, alert_color=[255, 0, 0], speed=3, **kwargs): """Set the color mode for a specific channel.""" # keyword arguments may have been forwarded from cli args and need parsing colors = list(colors) self._begin_transaction() if mode == 'rainbow': if isinstance(speed, str): speed = int(speed) self._write([0x23, clamp(speed, 1, 6)]) # make sure to clear blinking or... chaos self._configure_device(alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) elif mode == 'fading': self._configure_device(fading=True, color1=colors[0], color2=colors[1], interval1=clamp(time_per_color, 1, 255), alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) self._write([0x23, 0]) elif mode == 'blinking': if time_off is None: time_off = time_per_color self._configure_device(blinking=True, color1=colors[0], interval1=clamp(time_off, 1, 255), interval2=clamp(time_per_color, 1, 255), alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) self._write([0x23, 0]) elif mode == 'fixed': self._configure_device(color1=colors[0], alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) self._write([0x23, 0]) elif mode == 'blackout': # stronger than just 'off', suppresses alerts and rainbow self._configure_device(blackout=True, alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) else: raise KeyError(f'unknown lighting mode {mode}') self._end_transaction_and_read() def set_speed_profile(self, channel, profile, **kwargs): """Set channel to follow a speed duty profile.""" mtype, dmin, dmax = _VARIABLE_SPEED_CHANNELS[channel] adjusted = self._prepare_profile(profile, dmin, dmax) for temp, duty in adjusted: _LOGGER.info('setting %s PWM point: (%d°C, %d%%), device interpolated', channel, temp, duty) temps, duties = map(list, zip(*adjusted)) self._begin_transaction() self._write([mtype, 0] + temps + duties) self._end_transaction_and_read() def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" if channel == 'fan': # While devices seem to recognize a specific channel for fixed fan # speeds (mtype == 0x12), its use can later conflict with custom # profiles. # Note for a future self: the conflict can be cleared with # *another* call to initialize(), i.e. with another # configuration command. _LOGGER.info('using a flat profile to set %s to a fixed duty', channel) self.set_speed_profile(channel, [(0, duty), (_CRITICAL_TEMPERATURE - 1, duty)]) return mtype, dmin, dmax = _FIXED_SPEED_CHANNELS[channel] duty = clamp(duty, dmin, dmax) total_levels = _MAX_PUMP_SPEED_CODE - _MIN_PUMP_SPEED_CODE + 1 level = round((duty - dmin)/(dmax - dmin)*total_levels) effective_duty = round(dmin + level*(dmax - dmin)/total_levels) _LOGGER.info('setting %s PWM duty to %d%% (level %d)', channel, effective_duty, level) self._begin_transaction() self._write([mtype, _MIN_PUMP_SPEED_CODE + level]) self._end_transaction_and_read() class Legacy690Lc(_CommonAsetekDriver): """Legacy fifth generation Asetek 690LC cooler.""" SUPPORTED_DEVICES = [ (0x2433, 0xb200, None, 'Asetek 690LC (assuming NZXT Kraken X) (experimental)', {}), ] @classmethod def probe(cls, handle, legacy_690lc=False, **kwargs): if not legacy_690lc: return yield from super().probe(handle, **kwargs) def __init__(self, device, description, **kwargs): super().__init__(device, description, **kwargs) # --device causes drivers to be instantiated even if they are later # discarded; defer instantiating the data storage until to connect() self._data = None def connect(self, runtime_storage=None, **kwargs): ret = super().connect(**kwargs) if runtime_storage: self._data = runtime_storage else: ids = f'vid{self.vendor_id:04x}_pid{self.product_id:04x}' loc = f'bus{self.bus}_port{"_".join(map(str, self.port))}' self._data = RuntimeStorage(key_prefixes=[ids, loc, 'legacy']) return ret def _set_all_fixed_speeds(self): self._begin_transaction() for channel in ['pump', 'fan']: mtype, dmin, dmax = _LEGACY_FIXED_SPEED_CHANNELS[channel] duty = clamp(self._data.load_int(f'{channel}_duty', default=dmax), dmin, dmax) _LOGGER.info('setting %s duty to %d%%', channel, duty) self._write([mtype, duty]) return self._end_transaction_and_read() def initialize(self, **kwargs): super().initialize(**kwargs) self._data.store_int('pump_duty', None) self._data.store_int('fan_duty', None) self._set_all_fixed_speeds() def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ msg = self._set_all_fixed_speeds() firmware = '{}.{}.{}.{}'.format(*tuple(msg[0x17:0x1b])) return [ ('Liquid temperature', msg[10] + msg[14]/10, '°C'), ('Fan speed', msg[0] << 8 | msg[1], 'rpm'), ('Pump speed', msg[8] << 8 | msg[9], 'rpm'), ('Firmware version', firmware, '') ] def set_color(self, channel, mode, colors, time_per_color=None, time_off=None, alert_threshold=_HIGH_TEMPERATURE, alert_color=[255, 0, 0], **kwargs): """Set the color mode for a specific channel.""" # keyword arguments may have been forwarded from cli args and need parsing colors = list(colors) self._begin_transaction() if mode == 'fading': if time_per_color is None: time_per_color = 5 self._configure_device(fading=True, color1=colors[0], color2=colors[1], interval1=clamp(time_per_color, 1, 255), alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) elif mode == 'blinking': if time_per_color is None: time_per_color = 1 if time_off is None: time_off = time_per_color self._configure_device(blinking=True, color1=colors[0], interval1=clamp(time_off, 1, 255), interval2=clamp(time_per_color, 1, 255), alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) elif mode == 'fixed': self._configure_device(color1=colors[0], alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) elif mode == 'blackout': # stronger than just 'off', suppresses alerts and rainbow self._configure_device(blackout=True, alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) else: raise KeyError(f'unsupported lighting mode {mode}') self._end_transaction_and_read() def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" mtype, dmin, dmax = _LEGACY_FIXED_SPEED_CHANNELS[channel] duty = clamp(duty, dmin, dmax) self._data.store_int(f'{channel}_duty', duty) self._set_all_fixed_speeds() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice class Hydro690Lc(Modern690Lc): """Corsair-branded fifth generation Asetek 690LC cooler.""" SUPPORTED_DEVICES = [ (0x1b1c, 0x0c02, None, 'Corsair Hydro H80i GT (experimental)', {}), (0x1b1c, 0x0c03, None, 'Corsair Hydro H100i GTX (experimental)', {}), (0x1b1c, 0x0c07, None, 'Corsair Hydro H110i GTX (experimental)', {}), (0x1b1c, 0x0c08, None, 'Corsair Hydro H80i v2', {}), (0x1b1c, 0x0c09, None, 'Corsair Hydro H100i v2', {}), (0x1b1c, 0x0c0a, None, 'Corsair Hydro H115i', {}), ] @classmethod def probe(cls, handle, legacy_690lc=False, **kwargs): # the modern driver overrides probe and rigs it to switch on # --legacy-690lc, so we override it again return super().probe(handle, legacy_690lc=False, **kwargs) def set_color(self, channel, mode, colors, **kwargs): """Set the color mode for a specific channel.""" if mode == 'rainbow': raise KeyError(f'unsupported lighting mode {mode}') super().set_color(channel, mode, colors, **kwargs) # deprecated aliases AsetekDriver = Modern690Lc LegacyAsetekDriver = Legacy690Lc CorsairAsetekDriver = Hydro690Lc liquidctl-1.5.1/liquidctl/driver/base.py000066400000000000000000000107441401367561700203010ustar00rootroot00000000000000"""Base bus and driver API. Copyright (C) 2018–2019 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ class BaseDriver: """Base driver API. All drivers are expected to implement this API for compatibility with the liquidctl CLI or other thirdy party tools. Drivers will automatically implement the context manager protocol, but this should only be used from a call to `connect`. Example: for dev in .find_supported_devices(): with dev.connect(): print(dev.get_status()) if dev.serial_number == '49385027ZP': dev.set_fixed_speed('fan3', 42) """ @classmethod def find_supported_devices(cls, **kwargs): """Find and bind to compatible devices. Returns a list of bound driver instances. """ raise NotImplementedError() def connect(self, **kwargs): """Connect to the device. Procedure before any read or write operation can be performed. Typically a handshake between driver and device. Returns `self`. """ raise NotImplementedError() def initialize(self, **kwargs): """Initialize the device. Apart from `connect()`, some devices might require a onetime initialization procedure after powering on, or to detect hardware changes. This should be called *after* connecting to the device. This function can optionally return a list of `(property, value, unit)` tuples, similarly to `get_status`. """ raise NotImplementedError() def disconnect(self, **kwargs): """Disconnect from the device. Procedure before the driver can safely unbind from the device. Typically just cleanup. """ raise NotImplementedError() def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ raise NotImplementedError() def set_color(self, channel, mode, colors, **kwargs): """Set the color mode for a specific channel.""" raise NotImplementedError() def set_speed_profile(self, channel, profile, **kwargs): """Set channel to follow a speed duty profile.""" raise NotImplementedError() def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" raise NotImplementedError() @property def description(self): """Human readable description of the corresponding device.""" raise NotImplementedError() @property def vendor_id(self): """Numeric vendor identifier, or None if N/A.""" raise NotImplementedError() @property def product_id(self): """Numeric product identifier, or None if N/A.""" raise NotImplementedError() @property def release_number(self): """Device versioning number, or None if N/A. In USB devices this is bcdDevice. """ raise NotImplementedError() @property def serial_number(self): """Serial number reported by the device, or None if N/A.""" raise NotImplementedError() @property def bus(self): """Bus the device is connected to, or None if N/A.""" raise NotImplementedError() @property def address(self): """Address of the device on the corresponding bus, or None if N/A. This typically depends on the bus enumeration order. """ raise NotImplementedError() @property def port(self): """Physical location of the device, or None if N/A. This typically refers to a USB port, which is *not* dependent on bus enumeration order. However, a USB port is hub-specific, and hubs can be chained. Thus, for USB devices, this returns a tuple of port numbers, from the root hub to the parent of the connected device. """ raise NotImplementedError() def __enter__(self): return self def __exit__(self, *args): self.disconnect() class BaseBus: """Base bus API.""" def find_devices(self, **kwargs): """Find compatible devices and yield corresponding driver instances.""" return def find_all_subclasses(cls): """Recursively find loaded subclasses of `cls`. Returns a set of subclasses of `cls`. """ sub = set(cls.__subclasses__()) return sub.union([s for c in cls.__subclasses__() for s in find_all_subclasses(c)]) liquidctl-1.5.1/liquidctl/driver/commander_pro.py000066400000000000000000000451611401367561700222150ustar00rootroot00000000000000"""liquidctl drivers for Corsair Commander Pro devices. Supported devices: - Corsair Commander Pro - Corsair Lighting Node Pro NOTE: This device currently only has hardware control implemented but it also supports a software control mode. Software control will be enabled at a future time. Copyright (C) 2020–2021 Marshall Asch and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging import re from enum import Enum, unique from liquidctl.driver.usb import UsbHidDriver from liquidctl.keyval import RuntimeStorage from liquidctl.pmbus import compute_pec from liquidctl.util import clamp, fraction_of_byte, u16be_from, u16le_from, normalize_profile, check_unsafe from liquidctl.error import NotSupportedByDevice _LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 64 _RESPONSE_LENGTH = 16 _CMD_GET_FIRMWARE = 0x02 _CMD_GET_BOOTLOADER = 0x06 _CMD_GET_TEMP_CONFIG = 0x10 _CMD_GET_TEMP = 0x11 _CMD_GET_VOLTS = 0x12 _CMD_GET_FAN_MODES = 0x20 _CMD_GET_FAN_RPM = 0x21 _CMD_SET_FAN_DUTY = 0x23 _CMD_SET_FAN_PROFILE = 0x25 _CMD_RESET_LED_CHANNEL = 0x37 _CMD_BEGIN_LED_EFFECT = 0x34 _CMD_SET_LED_CHANNEL_STATE = 0x38 _CMD_LED_EFFECT = 0x35 _CMD_LED_COMMIT = 0x33 _LED_PORT_STATE_HARDWARE = 0x01 _LED_PORT_STATE_SOFTWARE = 0x02 _LED_SPEED_FAST = 0x00 _LED_SPEED_MEDIUM = 0x01 _LED_SPEED_SLOW = 0x02 _LED_DIRECTION_FORWARD = 0x01 _LED_DIRECTION_BACKWARD = 0x00 _FAN_MODE_DISCONNECTED = 0x00 _FAN_MODE_DC = 0x01 _FAN_MODE_PWM = 0x02 _PROFILE_LENGTH = 6 _CRITICAL_TEMPERATURE = 60 _CRITICAL_TEMPERATURE_HIGH = 100 _MAX_FAN_RPM = 5000 # I have no idea if this is a good value or not _MODES = { 'off': 0x04, # this is a special case of fixed 'rainbow': 0x00, 'color_shift': 0x01, 'color_pulse': 0x02, 'color_wave': 0x03, 'fixed': 0x04, # 'temperature': 0x05, # ignore this 'visor': 0x06, 'marquee': 0x07, 'blink': 0x08, 'sequential': 0x09, 'rainbow2': 0x0a, } def _prepare_profile(original, critcalTempature): clamped = ((temp, clamp(duty, 0, _MAX_FAN_RPM)) for temp, duty in original) normal = normalize_profile(clamped, critcalTempature, _MAX_FAN_RPM) missing = _PROFILE_LENGTH - len(normal) if missing < 0: raise ValueError(f'too many points in profile (remove {-missing})') if missing > 0: normal += missing * [(critcalTempature, _MAX_FAN_RPM)] return normal def _quoted(*names): return ', '.join(map(repr, names)) def _get_fan_mode_description(mode): """This will convert the fan mode value to a descriptive name. """ if mode == _FAN_MODE_DISCONNECTED: return 'Auto/Disconnected' elif mode == _FAN_MODE_DC: return 'DC' elif mode == _FAN_MODE_PWM: return 'PWM' else: return 'UNKNOWN' class CommanderPro(UsbHidDriver): """Corsair Commander Pro LED and fan hub""" SUPPORTED_DEVICES = [ (0x1b1c, 0x0c10, None, 'Corsair Commander Pro (experimental)', {'fan_count': 6, 'temp_probs': 4, 'led_channels': 2}), (0x1b1c, 0x0c0b, None, 'Corsair Lighting Node Pro (experimental)', {'fan_count': 0, 'temp_probs': 0, 'led_channels': 2}), ] def __init__(self, device, description, fan_count, temp_probs, led_channels, **kwargs): super().__init__(device, description, **kwargs) # the following fields are only initialized in connect() self._data = None self._fan_names = [f'fan{i+1}' for i in range(fan_count)] self._led_names = [f'led{i+1}' for i in range(led_channels)] self._temp_probs = temp_probs self._fan_count = fan_count def connect(self, runtime_storage=None, **kwargs): """Connect to the device.""" ret = super().connect(**kwargs) if runtime_storage: self._data = runtime_storage else: ids = f'vid{self.vendor_id:04x}_pid{self.product_id:04x}' # must use the HID path because there is no serial number; however, # these can be quite long on Windows and macOS, so only take the # numbers, since they are likely the only parts that vary between two # devices of the same model loc = 'loc' + '_'.join(re.findall(r'\d+', self.address)) self._data = RuntimeStorage(key_prefixes=[ids, loc]) return ret def initialize(self, **kwargs): """Initialize the device and get the fan modes. The device should be initialized every time it is powered on, including when the system resumes from suspending to memory. Returns a list of `(property, value, unit)` tuples. """ res = self._send_command(_CMD_GET_FIRMWARE) fw_version = (res[1], res[2], res[3]) res = self._send_command(_CMD_GET_BOOTLOADER) bootloader_version = (res[1], res[2]) # is it possible for there to be a third value? status = [ ('Firmware version', '{}.{}.{}'.format(*fw_version), ''), ('Bootloader version', '{}.{}'.format(*bootloader_version), ''), ] if self._temp_probs > 0: res = self._send_command(_CMD_GET_TEMP_CONFIG) temp_connected = res[1:5] self._data.store('temp_sensors_connected', temp_connected) status += [ ('Temp sensor 1', 'Connected' if temp_connected[0] else 'Not Connected', ''), ('Temp sensor 2', 'Connected' if temp_connected[1] else 'Not Connected', ''), ('Temp sensor 3', 'Connected' if temp_connected[2] else 'Not Connected', ''), ('Temp sensor 4', 'Connected' if temp_connected[3] else 'Not Connected', ''), ] if self._fan_count > 0: # get the information about how the fans are connected, probably want to save this for later res = self._send_command(_CMD_GET_FAN_MODES) fanModes = res[1:self._fan_count+1] self._data.store('fan_modes', fanModes) status += [ ('Fan 1 Mode', _get_fan_mode_description(fanModes[0]), ''), ('Fan 2 Mode', _get_fan_mode_description(fanModes[1]), ''), ('Fan 3 Mode', _get_fan_mode_description(fanModes[2]), ''), ('Fan 4 Mode', _get_fan_mode_description(fanModes[3]), ''), ('Fan 5 Mode', _get_fan_mode_description(fanModes[4]), ''), ('Fan 6 Mode', _get_fan_mode_description(fanModes[5]), ''), ] return status def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if self.device.product_id != 0x0c10: _LOGGER.debug('only the commander pro supports this') return [] connected_temp_sensors = self._data.load('temp_sensors_connected', default=[0]*self._temp_probs) fan_modes = self._data.load('fan_modes', default=[0]*self._fan_count) # get the temperature sensor values temp = [0]*self._temp_probs for num, enabled in enumerate(connected_temp_sensors): if enabled: temp[num] = self._get_temp(num) # get the real power supply voltages res = self._send_command(_CMD_GET_VOLTS, [0]) volt_12 = u16be_from(res, offset=1) / 1000 res = self._send_command(_CMD_GET_VOLTS, [1]) volt_5 = u16be_from(res, offset=1) / 1000 res = self._send_command(_CMD_GET_VOLTS, [2]) volt_3 = u16be_from(res, offset=1) / 1000 # get fan RPMs of connected fans fanspeeds = [0]*self._fan_count for fan_num, mode in enumerate(fan_modes): if mode == _FAN_MODE_DC or mode == _FAN_MODE_PWM: fanspeeds[fan_num] = self._get_fan_rpm(fan_num) status = [ ('12 volt rail', volt_12, 'V'), ('5 volt rail', volt_5, 'V'), ('3.3 volt rail', volt_3, 'V'), ] for temp_num in range(self._temp_probs): status += [(f'Temp sensor {temp_num + 1}', temp[temp_num], '°C')] for fan_num in range(self._fan_count): status += [(f'Fan {fan_num + 1} speed', fanspeeds[fan_num], 'rpm')] return status def _get_temp(self, sensor_num): """This will get the temperature in degrees celsius for the specified temp sensor. sensor number MUST be in range of 0-3 """ if self._temp_probs == 0: raise ValueError('this device does not have a temperature sensor') if sensor_num < 0 or sensor_num > 3: raise ValueError(f'sensor_num {sensor_num} invalid, must be between 0 and 3') res = self._send_command(_CMD_GET_TEMP, [sensor_num]) temp = u16be_from(res, offset=1) / 100 return temp def _get_fan_rpm(self, fan_num): """This will get the rpm value of the fan. fan number MUST be in range of 0-5 """ if self._fan_count == 0: raise ValueError('this device does not have any fans') if fan_num < 0 or fan_num > 5: raise ValueError(f'fan_num {fan_num} invalid, must be between 0 and 5') res = self._send_command(_CMD_GET_FAN_RPM, [fan_num]) speed = u16be_from(res, offset=1) return speed def _get_hw_fan_channels(self, channel): """This will get a list of all the fan channels that the command should be sent to It will look up the name of the fan channel given and return a list of the real fan number """ channel = channel.lower() if channel == 'sync': return [i for i in range(len(self._fan_names))] elif channel in self._fan_names: return [self._fan_names.index(channel)] else: raise ValueError(f'unknown channel, should be one of: {_quoted("sync", *self._fan_names)}') def _get_hw_led_channels(self, channel): """This will get a list of all the led channels that the command should be sent to It will look up the name of the led channel given and return a list of the real led device number """ channel = channel.lower() if channel == 'led': return [i for i in range(len(self._led_names))] elif channel in self._led_names: return [self._led_names.index(channel)] else: raise ValueError(f'unknown channel, should be one of: {_quoted("led", *self._led_names)}') def set_fixed_speed(self, channel, duty, **kwargs): """Set fan or fans to a fixed speed duty. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. Different commands for sending fixed percent (0x23) and fixed rpm (0x24) Probably want to use fixed percent for this untill the rpm flag is enabled. Can only send one fan command at a time, if fan mode is unset will need to send 6? messages (or 1 per enabled fan) """ if self._fan_count == 0: raise NotSupportedByDevice() duty = clamp(duty, 0, 100) fan_channels = self._get_hw_fan_channels(channel) fan_modes = self._data.load('fan_modes', default=[0]*self._fan_count) for fan in fan_channels: mode = fan_modes[fan] if mode == _FAN_MODE_DC or mode == _FAN_MODE_PWM: self._send_command(_CMD_SET_FAN_DUTY, [fan, duty]) def set_speed_profile(self, channel, profile, temperature_sensor=1, **kwargs): """Set fan or fans to follow a speed duty profile. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. Up to six (temperature, duty) pairs can be supplied in `profile`, with temperatures in Celsius and duty values in percentage. The last point should set the fan to 100% duty cycle, or be omitted; in the latter case the fan will be set to max out at 60°C. """ # send fan num, temp sensor, check to make sure it is actually enabled, and do not let the user send external sensor # 6 2-byte big endian temps (celsius * 100), then 6 2-byte big endian rpms # need to figure out how to find out what the max rpm is for the given fan if self._fan_count == 0: raise NotSupportedByDevice() profile = list(profile) criticalTemp = _CRITICAL_TEMPERATURE_HIGH if check_unsafe('high_temperature', **kwargs) else _CRITICAL_TEMPERATURE profile = _prepare_profile(profile, criticalTemp) # fan_type = kwargs['fan_type'] # need to make sure this is set temp_sensor = clamp(temperature_sensor, 1, self._temp_probs) sensors = self._data.load('temp_sensors_connected', default=[0]*self._temp_probs) if sensors[temp_sensor-1] != 1: raise ValueError('the specified temperature sensor is not connected') buf = bytearray(26) buf[1] = temp_sensor-1 # 0 # use temp sensor 1 for i, entry in enumerate(profile): temp = entry[0]*100 rpm = entry[1] # convert both values to 2 byte big endian values buf[2 + i*2] = temp.to_bytes(2, byteorder='big')[0] buf[3 + i*2] = temp.to_bytes(2, byteorder='big')[1] buf[14 + i*2] = rpm.to_bytes(2, byteorder='big')[0] buf[15 + i*2] = rpm.to_bytes(2, byteorder='big')[1] fan_channels = self._get_hw_fan_channels(channel) fan_modes = self._data.load('fan_modes', default=[0]*self._fan_count) for fan in fan_channels: mode = fan_modes[fan] if mode == _FAN_MODE_DC or mode == _FAN_MODE_PWM: buf[0] = fan self._send_command(_CMD_SET_FAN_PROFILE, buf) def set_color(self, channel, mode_str, colors, direction='forward', speed='medium', start_led=1, maximum_leds=1, **kwargs): """Set the color of each LED. In reality the device does not have the concept of different channels or modes, but this driver provides a few for convenience. Animations still require successive calls to this API. The 'led' channel can be used to address individual LEDs, and supports the 'super-fixed', 'fixed' and 'off' modes. In 'super-fixed' mode, each color in `colors` is applied to one individual LED, successively. LEDs for which no color has been specified default to off/solid black. This is closest to how the device works. In 'fixed' mode, all LEDs are set to the first color taken from `colors`. The `off` mode is equivalent to calling this function with 'fixed' and a single solid black color in `colors`. The `colors` argument should be an iterable of one or more `[red, blue, green]` triples, where each red/blue/green component is a value in the range 0–255. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | Num colors | | -------- | ----------- | ---------- | | led | off | 0 | | led | fixed | 1 | | led | color_shift | 2 | | led | color_pulse | 2 | | led | color_wave | 2 | | led | visor | 2 | | led | blink | 2 | | led | marquee | 1 | | led | sequential | 1 | | led | rainbow | 0 | | led | rainbow2 | 0 | """ # a special mode to clear the current led settings. # this is usefull if the the user wants to use a led mode for multiple devices if mode_str == 'clear': self._data.store('saved_effects', None) return colors = list(colors) expanded = colors[:3] c = itertools.chain(*((r, g, b) for r, g, b in expanded)) colors = list(c) direction = direction.lower() speed = speed.lower() channel = channel.lower() mode = mode_str.lower() # default to channel 1 if channel 2 is not specified. led_channel = 1 if channel == 'led2' else 0 direction = _LED_DIRECTION_FORWARD if direction == 'forward' else _LED_DIRECTION_BACKWARD speed = _LED_SPEED_SLOW if speed == 'slow' else _LED_SPEED_FAST if speed == 'fast' else _LED_SPEED_MEDIUM start_led = clamp(start_led, 1, 96) - 1 num_leds = clamp(maximum_leds, 1, 96-start_led-1) # there is a current firmware limitation of 96 led's per channel random_colors = 0x00 if mode_str == 'off' or len(colors) != 0 else 0x01 mode = _MODES.get(mode, -1) if mode == -1: raise ValueError(f'mode "{mode_str}" is not valid') lighting_effect = { 'channel': led_channel, 'start_led': start_led, 'num_leds': num_leds, 'mode': mode, 'speed': speed, 'direction': direction, 'random_colors': random_colors, 'colors': colors } saved_effects = [] if mode_str == 'off' else self._data.load('saved_effects', default=[]) saved_effects += [lighting_effect] self._data.store('saved_effects', None if mode_str == 'off' else saved_effects) # start sending the led commands self._send_command(_CMD_RESET_LED_CHANNEL, [led_channel]) self._send_command(_CMD_BEGIN_LED_EFFECT, [led_channel]) self._send_command(_CMD_SET_LED_CHANNEL_STATE, [led_channel, 0x01]) for effect in saved_effects: config = [effect.get('channel'), effect.get('start_led'), effect.get('num_leds'), effect.get('mode'), effect.get('speed'), effect.get('direction'), effect.get('random_colors'), 0xff ] + effect.get('colors') self._send_command(_CMD_LED_EFFECT, config) self._send_command(_CMD_LED_COMMIT, [0xff]) def _send_command(self, command, data=None): # self.device.write expects buf[0] to be the report number or 0 if not used buf = bytearray(_REPORT_LENGTH + 1) buf[1] = command start_at = 2 if data: data = data[:_REPORT_LENGTH-1] buf[start_at: start_at + len(data)] = data self.device.clear_enqueued_reports() self.device.write(buf) buf = bytes(self.device.read(_RESPONSE_LENGTH)) return buf liquidctl-1.5.1/liquidctl/driver/corsair_hid_psu.py000066400000000000000000000152041401367561700225400ustar00rootroot00000000000000"""liquidctl drivers for Corsair HXi and RMi series power supply units. Supported devices: - Corsair HXi (HX750i, HX850i, HX1000i and HX1200i) - Corsair RMi (RM650i, RM750i, RM850i and RM1000i) Copyright (C) 2019–2021 Jonas Malaco and contributors Port of corsaiRMi by notaz and realies. Copyright (c) notaz, 2016 Incorporates or uses as reference work by Sean Nelson. SPDX-License-Identifier: GPL-3.0-or-later """ import logging from datetime import timedelta from enum import Enum from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.pmbus import CommandCode as CMD from liquidctl.pmbus import WriteBit, linear_to_float from liquidctl.util import clamp _LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 64 _SLAVE_ADDRESS = 0x02 _CORSAIR_READ_TOTAL_UPTIME = CMD.MFR_SPECIFIC_D1 _CORSAIR_READ_UPTIME = CMD.MFR_SPECIFIC_D2 _CORSAIR_12V_OCP_MODE = CMD.MFR_SPECIFIC_D8 _CORSAIR_READ_INPUT_POWER = CMD.MFR_SPECIFIC_EE _CORSAIR_FAN_CONTROL_MODE = CMD.MFR_SPECIFIC_F0 _RAIL_12V = 0x0 _RAIL_5V = 0x1 _RAIL_3P3V = 0x2 _RAIL_NAMES = {_RAIL_12V: '+12V', _RAIL_5V: '+5V', _RAIL_3P3V: '+3.3V'} _MIN_FAN_DUTY = 0 class OCPMode(Enum): """Overcurrent protection mode.""" SINGLE_RAIL = 0x1 MULTI_RAIL = 0x2 def __str__(self): return self.name.capitalize().replace('_', ' ') class FanControlMode(Enum): """Fan control mode.""" HARDWARE = 0x0 SOFTWARE = 0x1 def __str__(self): return self.name.capitalize() class CorsairHidPsu(UsbHidDriver): """Corsair HXi or RMi series power supply unit.""" SUPPORTED_DEVICES = [ (0x1b1c, 0x1c05, None, 'Corsair HX750i', {}), (0x1b1c, 0x1c06, None, 'Corsair HX850i', {}), (0x1b1c, 0x1c07, None, 'Corsair HX1000i', {}), (0x1b1c, 0x1c08, None, 'Corsair HX1200i', {}), (0x1b1c, 0x1c0a, None, 'Corsair RM650i', {}), (0x1b1c, 0x1c0b, None, 'Corsair RM750i', {}), (0x1b1c, 0x1c0c, None, 'Corsair RM850i', {}), (0x1b1c, 0x1c0d, None, 'Corsair RM1000i', {}), ] def initialize(self, single_12v_ocp=False, **kwargs): """Initialize the device. Necessary to receive non-zero value responses from the device. Note: replies before calling this function appear to follow the pattern
. """ self.device.clear_enqueued_reports() self._write([0xfe, 0x03]) # not well understood self._read() mode = OCPMode.SINGLE_RAIL if single_12v_ocp else OCPMode.MULTI_RAIL if mode != self._get_12v_ocp_mode(): # TODO replace log level with info once this has been confimed to work _LOGGER.warning('(experimental feature) changing +12V OCP mode to %s', mode) self._exec(WriteBit.WRITE, _CORSAIR_12V_OCP_MODE, [mode.value]) if self._get_fan_control_mode() != FanControlMode.HARDWARE: _LOGGER.info('resetting fan control to hardware mode') self._set_fan_control_mode(FanControlMode.HARDWARE) def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ self.device.clear_enqueued_reports() ret = self._exec(WriteBit.WRITE, CMD.PAGE, [0]) if ret[1] == 0xfe: _LOGGER.warning('possibly uninitialized device') status = [ ('Current uptime', self._get_timedelta(_CORSAIR_READ_UPTIME), ''), ('Total uptime', self._get_timedelta(_CORSAIR_READ_TOTAL_UPTIME), ''), ('Temperature 1', self._get_float(CMD.READ_TEMPERATURE_1), '°C'), ('Temperature 2', self._get_float(CMD.READ_TEMPERATURE_2), '°C'), ('Fan control mode', self._get_fan_control_mode(), ''), ('Fan speed', self._get_float(CMD.READ_FAN_SPEED_1), 'rpm'), ('Input voltage', self._get_float(CMD.READ_VIN), 'V'), ('Total power', self._get_float(_CORSAIR_READ_INPUT_POWER), 'W'), ('+12V OCP mode', self._get_12v_ocp_mode(), ''), ] for rail in [_RAIL_12V, _RAIL_5V, _RAIL_3P3V]: name = _RAIL_NAMES[rail] self._exec(WriteBit.WRITE, CMD.PAGE, [rail]) status.append((f'{name} output voltage', self._get_float(CMD.READ_VOUT), 'V')) status.append((f'{name} output current', self._get_float(CMD.READ_IOUT), 'A')) status.append((f'{name} output power', self._get_float(CMD.READ_POUT), 'W')) self._exec(WriteBit.WRITE, CMD.PAGE, [0]) _LOGGER.warning('reading the +12V OCP mode is an experimental feature') return status def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" duty = clamp(duty, _MIN_FAN_DUTY, 100) _LOGGER.info('ensuring fan control is in software mode') self._set_fan_control_mode(FanControlMode.SOFTWARE) _LOGGER.info('setting fan PWM duty to %d%%', duty) self._exec(WriteBit.WRITE, CMD.FAN_COMMAND_1, [duty]) def set_color(self, channel, mode, colors, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def _write(self, data): assert len(data) <= _REPORT_LENGTH packet = bytearray(1 + _REPORT_LENGTH) packet[1: 1 + len(data)] = data # device doesn't use numbered reports self.device.write(packet) def _read(self): return self.device.read(_REPORT_LENGTH) def _exec(self, writebit, command, data=None): self._write([_SLAVE_ADDRESS | WriteBit(writebit), CMD(command)] + (data or [])) return self._read() def _get_12v_ocp_mode(self): """Get +12V single/multi-rail OCP mode.""" return OCPMode(self._exec(WriteBit.READ, _CORSAIR_12V_OCP_MODE)[2]) def _get_fan_control_mode(self): """Get hardware/software fan control mode.""" return FanControlMode(self._exec(WriteBit.READ, _CORSAIR_FAN_CONTROL_MODE)[2]) def _set_fan_control_mode(self, mode): """Set hardware/software fan control mode.""" return self._exec(WriteBit.WRITE, _CORSAIR_FAN_CONTROL_MODE, [mode.value]) def _get_float(self, command): """Get float value with `command`.""" return linear_to_float(self._exec(WriteBit.READ, command)[2:]) def _get_timedelta(self, command): """Get timedelta with `command`.""" secs = int.from_bytes(self._exec(WriteBit.READ, command)[2:], byteorder='little') return timedelta(seconds=secs) # deprecated aliases CorsairHidPsuDriver = CorsairHidPsu liquidctl-1.5.1/liquidctl/driver/ddr4.py000066400000000000000000000304701401367561700202220ustar00rootroot00000000000000"""liquidctl drivers for DDR4 memory. Copyright (C) 2020–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ from enum import Enum, unique from collections import namedtuple import itertools import logging from liquidctl.driver.smbus import SmbusDriver from liquidctl.error import ExpectationNotMet, NotSupportedByDevice, NotSupportedByDriver from liquidctl.util import check_unsafe, clamp _LOGGER = logging.getLogger(__name__) class Ddr4Spd: """Partial decoding of DDR4 Serial Presence Detect (SPD) information. Properties will raise on data they are not yet prepared to handle, but what is implemented attempts to comply with JEDEC 21-C 4.1.2 Annex L. """ class DramDeviceType(Enum): """DRAM device type (not exhaustive).""" DDR4_SDRAM = 0x0c LPDDR4_SDRAM = 0x10 LPDDR4X_SDRAM = 0x11 def __str__(self): return self.name.replace('_', ' ') class BaseModuleType(Enum): """Base module type (not exhaustive).""" RDIMM = 0b0001 UDIMM = 0b0010 SO_DIMM = 0b0011 LRDIMM = 0x0100 def __str__(self): return self.name.replace('_', ' ') # Standard Manufacturer's Identification Code from JEDEC JEP106; # (not exhaustive) maps banks and IDs to names: _JEP106[][id] _JEP106 = { 1: { 0x2c: 'Micron', 0xad: 'SK Hynix', 0xce: 'Samsung', }, 2: {0x98: 'Kingston'}, 3: {0x9e: 'Corsair'}, 5: { 0xcd: 'G.SKILL', 0xef: 'Team Group', }, 6: { 0x02: 'Patriot', 0x9b: 'Crucial', }, } def __init__(self, eeprom): self._eeprom = eeprom if self.dram_device_type not in [self.DramDeviceType.DDR4_SDRAM, self.DramDeviceType.LPDDR4_SDRAM, self.DramDeviceType.LPDDR4X_SDRAM]: raise ValueError('not a DDR4 SPD EEPROM') @property def spd_bytes_used(self): nibble = self._eeprom[0x00] & 0x0f assert nibble <= 0b0100, 'reserved' return nibble * 128 @property def spd_bytes_total(self): nibble = (self._eeprom[0x00] >> 4) & 0b111 assert nibble <= 0b010, 'reserved' return nibble * 256 @property def spd_revision(self): enc_level = self._eeprom[0x01] >> 4 add_level = self._eeprom[0x01] & 0x0f return (enc_level, add_level) @property def dram_device_type(self): return self.DramDeviceType(self._eeprom[0x02]) @property def module_type(self): base = self._eeprom[0x03] & 0x0f hybrid = self._eeprom[0x03] >> 4 assert not hybrid return (self.BaseModuleType(base), None) @property def module_thermal_sensor(self): present = self._eeprom[0x0e] >> 7 return bool(present) @property def module_manufacturer(self): bank = 1 + self._eeprom[0x140] & 0x7f mid = self._eeprom[0x141] return self._JEP106[bank][mid] @property def module_part_number(self): return self._eeprom[0x149:0x15d].decode(encoding='ascii').rstrip() @property def dram_manufacturer(self): bank = 1 + self._eeprom[0x15e] & 0x7f mid = self._eeprom[0x15f] return self._JEP106[bank][mid] class Ddr4Temperature(SmbusDriver): """DDR4 module with TSE2004-compatible SPD EEPROM and temperature sensor.""" _SPD_DTIC = 0x50 _TS_DTIC = 0x18 _SA_MASK = 0b111 _REG_CAPABILITIES = 0x00 _REG_TEMPERATURE = 0x05 _UNSAFE = ['smbus', 'ddr4_temperature'] @classmethod def probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): # FIXME support mainstream AMD chipsets on Linux; note that unlike # i801_smbus, piix4_smbus does not enumerate and register the available # SPD EEPROMs with i2c_register_spd _SMBUS_DRIVERS = ['i801_smbus'] if smbus.parent_driver not in _SMBUS_DRIVERS \ or any([vendor, product, release, serial]): # wont match, always None return for dimm in range(cls._SA_MASK + 1): spd_addr = cls._SPD_DTIC | dimm eeprom = smbus.load_eeprom(spd_addr) if not eeprom or eeprom.name != 'ee1004': continue try: spd = Ddr4Spd(eeprom.data) if spd.dram_device_type != Ddr4Spd.DramDeviceType.DDR4_SDRAM: continue desc = cls._match(spd) except: continue if not desc: continue desc += f' DIMM{dimm + 1} (experimental)' if (address and int(address, base=16) != spd_addr) \ or (match and match.lower() not in desc.lower()): continue # set the default device address to a weird value to prevent # accidental attempts of writes to the SPD EEPROM (DDR4 SPD writes # are also disabled by default in many motherboards) dev = cls(smbus, desc, address=(None, None, spd_addr)) _LOGGER.debug('instanced driver for %s', desc) yield dev @classmethod def _match(cls, spd): if not spd.module_thermal_sensor: return None try: manufacturer = spd.module_manufacturer except: return 'DDR4' if spd.module_part_number: return f'{manufacturer} {spd.module_part_number}' else: return manufacturer def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._ts_address = self._TS_DTIC | (self._address[2] & self._SA_MASK) @property def address(self): return f'{self._address[2]:#04x}' def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if not check_unsafe(*self._UNSAFE, **kwargs): _LOGGER.warning("%s: nothing to return, requires unsafe features '%s'", self.description, ','.join(self._UNSAFE)) return [] treg = self._read_temperature_register() # discard flags bits and interpret remaining bits as 2s complement treg = treg & 0x1fff if treg > 0x0fff: treg -= 0x2000 # should always be supported resolution, bits = (.25, 10) multiplier = treg >> (12 - bits) return [ ('Temperature', resolution * multiplier, '°C'), ] def _read_temperature_register(self): return self._smbus.read_block_data(self._ts_address, self._REG_TEMPERATURE) def initialize(self, **kwargs): """Initialize the device.""" pass def set_color(self, channel, mode, colors, **kwargs): """Not supported by this driver.""" raise NotSupportedByDriver() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() class VengeanceRgb(Ddr4Temperature): """Corsair Vengeance RGB DDR4 module.""" _RGB_DTIC = 0x58 _REG_RGB_TIMING1 = 0xa4 _REG_RGB_TIMING2 = 0xa5 _REG_RGB_MODE = 0xa6 _REG_RGB_COLOR_COUNT = 0xa7 _REG_RGB_COLOR_START = 0xb0 _REG_RGB_COLOR_END = 0xc5 _UNSAFE = ['smbus', 'vengeance_rgb'] @unique class Mode(bytes, Enum): def __new__(cls, value, min_colors, max_colors): obj = bytes.__new__(cls, [value]) obj._value_ = value obj.min_colors = min_colors obj.max_colors = max_colors return obj FIXED = (0x00, 1, 1) FADING = (0x01, 2, 7) BREATHING = (0x02, 1, 7) OFF = (0xf0, 0, 0) # pseudo mode, equivalent to fixed #000000 def __str__(self): return self.name.lower() @unique class SpeedTimings(Enum): SLOWEST = 63 SLOWER = 48 NORMAL = 32 FASTER = 16 FASTEST = 1 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._rgb_address = None @classmethod def _match(cls, spd): if spd.module_type != (Ddr4Spd.BaseModuleType.UDIMM, None) \ or spd.module_manufacturer != 'Corsair' \ or not spd.module_part_number.startswith('CMR'): return None return 'Corsair Vengeance RGB' def _read_temperature_register(self): # instead of using block reads, Vengeance RGB temperature sensor # devices must be read in words treg = self._smbus.read_word_data(self._ts_address, self._REG_TEMPERATURE) # swap LSB and MSB before returning: read_word_data reads in little # endianess, but the register should be read in big endianess return ((treg & 0xff) << 8) | (treg >> 8) def set_color(self, channel, mode, colors, speed='normal', transition_ticks=None, stable_ticks=None, **kwargs): """Set the RGB lighting mode and, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Colors | | -------- | --------- | ------ | | led | off | 0 | | led | fixed | 1 | | led | breathing | 1–7 | | led | fading | 2–7 | The speed of the breathing and fading animations can be adjusted with `speed`; the allowed values are 'slowest', 'slower', 'normal' (default), 'faster' and 'fastest'. It is also possible to override the raw timing parameters through `transition_ticks` and `stable_ticks`; these should be integer values in the range 0–63. """ check_unsafe(*self._UNSAFE, error=True, **kwargs) try: common = self.SpeedTimings[speed.upper()].value tp1 = tp2 = common except KeyError: raise ValueError(f'invalid speed preset: {speed!r}') from None if transition_ticks is not None: tp1 = clamp(transition_ticks, 0, 63) if stable_ticks is not None: tp2 = clamp(stable_ticks, 0, 63) colors = list(colors) try: mode = self.Mode[mode.upper()] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.min_colors: raise ValueError(f'{mode} mode requires {mode.min_colors} colors') if len(colors) > mode.max_colors: _LOGGER.debug('too many colors, dropping to %d', mode.max_colors) colors = colors[:mode.max_colors] self._compute_rgb_address() if mode == self.Mode.OFF: mode = self.Mode.FIXED colors = [[0x00, 0x00, 0x00]] def rgb_write(register, value): self._smbus.write_byte_data(self._rgb_address, register, value) if mode == self.Mode.FIXED: rgb_write(self._REG_RGB_TIMING1, 0x00) else: rgb_write(self._REG_RGB_TIMING1, tp1) rgb_write(self._REG_RGB_TIMING2, tp2) color_registers = range(self._REG_RGB_COLOR_START, self._REG_RGB_COLOR_END) color_components = itertools.chain(*colors) for register, component in zip(color_registers, color_components): rgb_write(register, component) rgb_write(self._REG_RGB_COLOR_COUNT, len(colors)) if mode == self.Mode.BREATHING and len(colors) == 1: rgb_write(self._REG_RGB_MODE, self.Mode.FIXED.value) else: rgb_write(self._REG_RGB_MODE, mode.value) def _compute_rgb_address(self): if self._rgb_address: return # the dimm's rgb controller is typically at 0x58–0x5f candidate = self._RGB_DTIC | (self._address[2] & self._SA_MASK) # reading from any register should return 0xba if we have the right device if self._smbus.read_byte_data(candidate, self._REG_RGB_MODE) != 0xba: raise ExpectationNotMet(f'{self.bus}:{candidate:#04x} is not the RGB controller') self._rgb_address = candidate liquidctl-1.5.1/liquidctl/driver/hydro_platinum.py000066400000000000000000000363561401367561700224340ustar00rootroot00000000000000"""liquidctl drivers for Corsair Hydro Platinum and Pro XT liquid coolers. Supported devices: - Corsair H100i Platinum - Corsair H100i Platinum SE - Corsair H115i Platinum - Corsair H100i Pro XT - Corsair H115i Pro XT Copyright (C) 2020–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging import re from enum import Enum, unique from liquidctl.driver.usb import UsbHidDriver from liquidctl.keyval import RuntimeStorage from liquidctl.pmbus import compute_pec from liquidctl.util import clamp, fraction_of_byte, u16le_from, \ normalize_profile, check_unsafe _LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 64 _WRITE_PREFIX = 0x3f _FEATURE_COOLING = 0b000 _CMD_GET_STATUS = 0xff _CMD_SET_COOLING = 0x14 _FEATURE_LIGHTING = None _CMD_SET_LIGHTING1 = 0b100 _CMD_SET_LIGHTING2 = 0b101 _CMD_SET_LIGHTING3 = 0b110 # cooling data starts at offset 3 and ends just before the PEC byte _SET_COOLING_DATA_LENGTH = _REPORT_LENGTH - 4 _SET_COOLING_DATA_PREFIX = [0x0, 0xff, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff] _FAN_MODE_OFFSETS = [0x0b - 3, 0x11 - 3] _FAN_DUTY_OFFSETS = [offset + 5 for offset in _FAN_MODE_OFFSETS] _FAN_PROFILE_OFFSETS = [0x1e - 3, 0x2c - 3] _FAN_OFFSETS = list(zip(_FAN_MODE_OFFSETS, _FAN_DUTY_OFFSETS, _FAN_PROFILE_OFFSETS)) _PUMP_MODE_OFFSET = 0x17 - 3 _PROFILE_LENGTH_OFFSET = 0x1d - 3 _PROFILE_LENGTH = 7 _CRITICAL_TEMPERATURE = 60 @unique class _FanMode(Enum): CUSTOM_PROFILE = 0x0 CUSTOM_PROFILE_WITH_EXTERNAL_SENSOR = 0x1 FIXED_DUTY = 0x2 FIXED_RPM = 0x4 @classmethod def _missing_(cls, value): _LOGGER.debug('falling back to FIXED_DUTY for _FanMode(%s)', value) return _FanMode.FIXED_DUTY @unique class _PumpMode(Enum): QUIET = 0x0 BALANCED = 0x1 EXTREME = 0x2 @classmethod def _missing_(cls, value): _LOGGER.debug('falling back to BALANCED for _PumpMode(%s)', value) return _PumpMode.BALANCED def _sequence(storage): """Return a generator that produces valid protocol sequence numbers. Sequence numbers increment across successful invocations of liquidctl, but are not atomic. The sequence is: 1, 2, 3... 29, 30, 31, 1, 2, 3... In the protocol the sequence number is usually shifted left by 3 bits, and a shifted sequence will look like: 8, 16, 24... 232, 240, 248, 8, 16, 24... """ while True: seq = storage.load('sequence', of_type=int, default=0) % 31 + 1 storage.store('sequence', seq) yield seq def _prepare_profile(original): clamped = ((temp, clamp(duty, 0, 100)) for temp, duty in original) normal = normalize_profile(clamped, _CRITICAL_TEMPERATURE) missing = _PROFILE_LENGTH - len(normal) if missing < 0: raise ValueError(f'too many points in profile (remove {-missing})') if missing > 0: normal += missing * [(_CRITICAL_TEMPERATURE, 100)] return normal def _quoted(*names): return ', '.join(map(repr, names)) class HydroPlatinum(UsbHidDriver): """Corsair Hydro Platinum or Pro XT liquid cooler.""" SUPPORTED_DEVICES = [ (0x1b1c, 0x0c18, None, 'Corsair H100i Platinum (experimental)', {'fan_count': 2, 'fan_leds': 4}), (0x1b1c, 0x0c19, None, 'Corsair H100i Platinum SE (experimental)', {'fan_count': 2, 'fan_leds': 16}), (0x1b1c, 0x0c17, None, 'Corsair H115i Platinum (experimental)', {'fan_count': 2, 'fan_leds': 4}), (0x1b1c, 0x0c20, None, 'Corsair H100i Pro XT (experimental)', {'fan_count': 2, 'fan_leds': 0}), (0x1b1c, 0x0c21, None, 'Corsair H115i Pro XT (experimental)', {'fan_count': 2, 'fan_leds': 0}), ] def __init__(self, device, description, fan_count, fan_leds, **kwargs): super().__init__(device, description, **kwargs) self._led_count = 16 + fan_count * fan_leds self._fan_names = [f'fan{i + 1}' for i in range(fan_count)] self._mincolors = { ('led', 'super-fixed'): 1, ('led', 'fixed'): 1, ('led', 'off'): 0, } self._maxcolors = { ('led', 'super-fixed'): self._led_count, ('led', 'fixed'): 1, ('led', 'off'): 0, } # the following fields are only initialized in connect() self._data = None self._sequence = None def connect(self, runtime_storage=None, **kwargs): """Connect to the device.""" ret = super().connect(**kwargs) if runtime_storage: self._data = runtime_storage else: ids = f'vid{self.vendor_id:04x}_pid{self.product_id:04x}' # must use the HID path because there is no serial number; however, # these can be quite long on Windows and macOS, so only take the # numbers, since they are likely the only parts that vary between two # devices of the same model loc = 'loc' + '_'.join(re.findall(r'\d+', self.address)) self._data = RuntimeStorage(key_prefixes=[ids, loc]) self._sequence = _sequence(self._data) return ret def initialize(self, pump_mode='balanced', **kwargs): """Initialize the device and set the pump mode. The device should be initialized every time it is powered on, including when the system resumes from suspending to memory. Valid values for `pump_mode` are 'quiet', 'balanced' and 'extreme'. Unconfigured fan channels may default to 100% duty. Subsequent calls should leave the fan speeds unaffected. Returns a list of `(property, value, unit)` tuples. """ # set the flag so the LED command will need to be set again self._data.store('leds_enabled', 0) self._data.store('pump_mode', _PumpMode[pump_mode.upper()].value) res = self._send_set_cooling() fw_version = (res[2] >> 4, res[2] & 0xf, res[3]) if fw_version < (1, 1, 0): # see: #201 ("Fan settings affects Fan 1 only and disables fan2") _LOGGER.warning('outdated and possibly unsupported firmware version') return [('Firmware version', '{}.{}.{}'.format(*fw_version), '')] def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ res = self._send_command(_FEATURE_COOLING, _CMD_GET_STATUS) assert len(self._fan_names) == 2, f'cannot yet parse with {len(self._fan_names)} fans' return [ ('Liquid temperature', res[8] + res[7] / 255, '°C'), ('Fan 1 speed', u16le_from(res, offset=15), 'rpm'), ('Fan 2 speed', u16le_from(res, offset=22), 'rpm'), ('Pump speed', u16le_from(res, offset=29), 'rpm'), ] def set_fixed_speed(self, channel, duty, **kwargs): """Set fan or fans to a fixed speed duty. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. """ for hw_channel in self._get_hw_fan_channels(channel): self._data.store(f'{hw_channel}_mode', _FanMode.FIXED_DUTY.value) self._data.store(f'{hw_channel}_duty', duty) self._send_set_cooling() def set_speed_profile(self, channel, profile, **kwargs): """Set fan or fans to follow a speed duty profile. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. Up to seven (temperature, duty) pairs can be supplied in `profile`, with temperatures in Celsius and duty values in percentage. The last point should set the fan to 100% duty cycle, or be omitted; in the latter case the fan will be set to max out at 60°C. """ profile = list(profile) for hw_channel in self._get_hw_fan_channels(channel): self._data.store(f'{hw_channel}_mode', _FanMode.CUSTOM_PROFILE.value) self._data.store(f'{hw_channel}_profile', profile) self._send_set_cooling() def set_color(self, channel, mode, colors, **kwargs): """Set the color of each LED. In reality the device does not have the concept of different channels or modes, but this driver provides a few for convenience. Animations still require successive calls to this API. The 'led' channel can be used to address individual LEDs, and supports the 'super-fixed', 'fixed' and 'off' modes. In 'super-fixed' mode, each color in `colors` is applied to one individual LED, successively. LEDs for which no color has been specified default to off/solid black. This is closest to how the device works. In 'fixed' mode, all LEDs are set to the first color taken from `colors`. The `off` mode is equivalent to calling this function with 'fixed' and a single solid black color in `colors`. The `colors` argument should be an iterable of one or more `[red, blue, green]` triples, where each red/blue/green component is a value in the range 0–255. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | LEDs | Platinum | Pro XT | Platinum SE | | -------- | ----------- | ------------ | -------- | ------ | ----------- | | led | off | synchronized | 0 | 0 | 0 | | led | fixed | synchronized | 1 | 1 | 1 | | led | super-fixed | independent | 24 | 16 | 48 | Note: lighting control of Pro XT devices is experimental and requires the `pro_xt_lighting` constant to be supplied in the `unsafe` iterable. """ if 'Pro XT' in self.description: check_unsafe('pro_xt_lighting', error=True, **kwargs) channel, mode, colors = channel.lower(), mode.lower(), list(colors) self._check_color_args(channel, mode, colors) if mode == 'off': expanded = [] elif (channel, mode) == ('led', 'super-fixed'): expanded = colors[:self._led_count] elif (channel, mode) == ('led', 'fixed'): expanded = list(itertools.chain(*([color] * self._led_count for color in colors[:1]))) else: assert False, 'assumed unreacheable' if self._data.load('leds_enabled', of_type=int, default=0) == 0: # These hex strings are currently magic values that work but Im not quite sure why. d1 = bytes.fromhex("0101ffffffffffffffffffffffffff7f7f7f7fff00ffffffff00ffffffff00ffffffff00ffffffff00ffffffff00ffffffffffffffffffffffffffffff") d2 = bytes.fromhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627ffffffffffffffffffffffffffffffffffffffffff") d3 = bytes.fromhex("28292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4fffffffffffffffffffffffffffffffffffffffffff") # Send the magic messages to enable setting the LEDs to statuC values self._send_command(None, 0b001, data=d1) self._send_command(None, 0b010, data=d2) self._send_command(None, 0b011, data=d3) self._data.store('leds_enabled', 1) data1 = bytes(itertools.chain(*((b, g, r) for r, g, b in expanded[0:20]))) data2 = bytes(itertools.chain(*((b, g, r) for r, g, b in expanded[20:40]))) data3 = bytes(itertools.chain(*((b, g, r) for r, g, b in expanded[40:]))) self._send_command(_FEATURE_LIGHTING, _CMD_SET_LIGHTING1, data=data1) if self._led_count > 20: self._send_command(_FEATURE_LIGHTING, _CMD_SET_LIGHTING2, data=data2) if self._led_count > 40: self._send_command(_FEATURE_LIGHTING, _CMD_SET_LIGHTING3, data=data3) def _check_color_args(self, channel, mode, colors): try: mincolors = self._mincolors[(channel, mode)] maxcolors = self._maxcolors[(channel, mode)] except KeyError: raise ValueError(f'unsupported (channel, mode) pair, ' f'should be one of: {_quoted(*self._mincolors)}') from None if len(colors) < mincolors: raise ValueError(f'at least {mincolors} required for {_quoted(channel, mode)}') if len(colors) > maxcolors: _LOGGER.warning('too many colors, dropping to %d', maxcolors) return maxcolors return len(colors) def _get_hw_fan_channels(self, channel): channel = channel.lower() if channel == 'fan': return self._fan_names if channel in self._fan_names: return [channel] raise ValueError(f'unknown channel, should be one of: {_quoted("fan", *self._fan_names)}') def _send_command(self, feature, command, data=None): # self.device.write expects buf[0] to be the report number or 0 if not used buf = bytearray(_REPORT_LENGTH + 1) buf[1] = _WRITE_PREFIX buf[2] = next(self._sequence) << 3 if feature is not None: buf[2] |= feature buf[3] = command start_at = 4 else: buf[2] |= command start_at = 3 if data: buf[start_at: start_at + len(data)] = data buf[-1] = compute_pec(buf[2:-1]) self.device.clear_enqueued_reports() self.device.write(buf) buf = bytes(self.device.read(_REPORT_LENGTH)) if compute_pec(buf[1:]): _LOGGER.warning('response checksum does not match data') return buf def _send_set_cooling(self): assert len(self._fan_names) <= 2, 'cannot yet fit all fan data' data = bytearray(_SET_COOLING_DATA_LENGTH) data[0: len(_SET_COOLING_DATA_PREFIX)] = _SET_COOLING_DATA_PREFIX data[_PROFILE_LENGTH_OFFSET] = _PROFILE_LENGTH for fan, (imode, iduty, iprofile) in zip(self._fan_names, _FAN_OFFSETS): mode = _FanMode(self._data.load(f'{fan}_mode', of_type=int)) if mode is _FanMode.FIXED_DUTY: stored = self._data.load(f'{fan}_duty', of_type=int, default=100) duty = clamp(stored, 0, 100) data[iduty] = fraction_of_byte(percentage=duty) _LOGGER.info('setting %s to %d%% duty cycle', fan, duty) elif mode is _FanMode.CUSTOM_PROFILE: stored = self._data.load(f'{fan}_profile', of_type=list, default=[]) profile = _prepare_profile(stored) # ensures correct len(profile) pairs = ((temp, fraction_of_byte(percentage=duty)) for temp, duty in profile) data[iprofile: iprofile + _PROFILE_LENGTH * 2] = itertools.chain(*pairs) _LOGGER.info('setting %s to follow profile %r', fan, profile) else: raise ValueError(f'unsupported fan {mode}') data[imode] = mode.value pump_mode = _PumpMode(self._data.load('pump_mode', of_type=int)) data[_PUMP_MODE_OFFSET] = pump_mode.value _LOGGER.info('setting pump mode to %s', pump_mode.name.lower()) return self._send_command(_FEATURE_COOLING, _CMD_SET_COOLING, data=data) liquidctl-1.5.1/liquidctl/driver/kraken2.py000066400000000000000000000241541401367561700207240ustar00rootroot00000000000000"""liquidctl drivers for third generation NZXT Kraken X and M liquid coolers. Kraken X (X42, X52, X62 and X72) -------------------------------- These coolers house 5-th generation Asetek pumps with additional PCBs for advanced control and RGB capabilites. Kraken M22 ---------- The Kraken M22 shares similar RGB funcionality to the X models of the same generation, but has no liquid temperature sensor and no hability to report or set fan or pump speeds. Copyright (C) 2018–2021 Jonas Malaco and contributors Incorporates work by leaty, Ksenija Stanojevic, Alexander Tong and Jens Neumaier. SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import clamp, normalize_profile, interpolate_profile _LOGGER = logging.getLogger(__name__) _SPEED_CHANNELS = { # (base, minimum duty, maximum duty) 'fan': (0x80, 25, 100), 'pump': (0xc0, 50, 100), } _CRITICAL_TEMPERATURE = 60 _COLOR_CHANNELS = { 'sync': 0x0, 'logo': 0x1, 'ring': 0x2, } _COLOR_MODES = { # (byte3/mode, byte2/reverse, byte4/modifier, min colors, max colors, only ring) 'off': (0x00, 0x00, 0x00, 0, 0, False), 'fixed': (0x00, 0x00, 0x00, 1, 1, False), 'super-fixed': (0x00, 0x00, 0x00, 1, 9, False), # independent logo + ring leds 'fading': (0x01, 0x00, 0x00, 2, 8, False), 'spectrum-wave': (0x02, 0x00, 0x00, 0, 0, False), 'marquee-3': (0x03, 0x00, 0x00, 1, 1, True), 'marquee-4': (0x03, 0x00, 0x08, 1, 1, True), 'marquee-5': (0x03, 0x00, 0x10, 1, 1, True), 'marquee-6': (0x03, 0x00, 0x18, 1, 1, True), 'covering-marquee': (0x04, 0x00, 0x00, 1, 8, True), 'alternating': (0x05, 0x00, 0x00, 2, 2, True), 'moving-alternating': (0x05, 0x08, 0x00, 2, 2, True), 'breathing': (0x06, 0x00, 0x00, 1, 8, False), # colors for each step 'super-breathing': (0x06, 0x00, 0x00, 1, 9, False), # one step, independent logo + ring leds 'pulse': (0x07, 0x00, 0x00, 1, 8, False), 'tai-chi': (0x08, 0x00, 0x00, 2, 2, True), 'water-cooler': (0x09, 0x00, 0x00, 0, 0, True), 'loading': (0x0a, 0x00, 0x00, 1, 1, True), 'wings': (0x0c, 0x00, 0x00, 1, 1, True), 'super-wave': (0x0d, 0x00, 0x00, 1, 8, True), # independent ring leds } _ANIMATION_SPEEDS = { 'slowest': 0x0, 'slower': 0x1, 'normal': 0x2, 'faster': 0x3, 'fastest': 0x4, } _READ_ENDPOINT = 0x81 _READ_LENGTH = 64 _WRITE_ENDPOINT = 0x1 _WRITE_LENGTH = 65 class Kraken2(UsbHidDriver): """Third generation NZXT Kraken X or M liquid cooler.""" DEVICE_KRAKENX = 'Kraken X' DEVICE_KRAKENM = 'Kraken M' SUPPORTED_DEVICES = [ (0x1e71, 0x170e, None, 'NZXT Kraken X (X42, X52, X62 or X72)', { 'device_type': DEVICE_KRAKENX }), (0x1e71, 0x1715, None, 'NZXT Kraken M22', { 'device_type': DEVICE_KRAKENM }), ] def __init__(self, device, description, device_type=DEVICE_KRAKENX, **kwargs): super().__init__(device, description) self.device_type = device_type self.supports_lighting = True self.supports_cooling = self.device_type != self.DEVICE_KRAKENM self._supports_cooling_profiles = None # physical storage/later inferred from fw version self._connected = False def connect(self, **kwargs): ret = super().connect(**kwargs) self._connected = True return ret def disconnect(self, **kwargs): super().disconnect(**kwargs) self._connected = False def initialize(self, **kwargs): # before v1.1 `initialize` was used to connect to the device; that has # since been deprecated, but we have to support that usage until v2 if not self._connected: self.connect(**kwargs) def finalize(self): """Deprecated.""" _LOGGER.warning('deprecated: use disconnect() instead') if self._connected: self.disconnect() def get_status(self, **kwargs): """Get a status report. Returns a list of (key, value, unit) tuples. """ msg = self._read() firmware = '{}.{}.{}'.format(*self._firmware_version) if self.device_type == self.DEVICE_KRAKENM: return [('Firmware version', firmware, '')] else: return [ ('Liquid temperature', msg[1] + msg[2]/10, '°C'), ('Fan speed', msg[3] << 8 | msg[4], 'rpm'), ('Pump speed', msg[5] << 8 | msg[6], 'rpm'), ('Firmware version', firmware, '') ] def set_color(self, channel, mode, colors, speed='normal', direction='forward', **kwargs): """Set the color mode for a specific channel.""" if not self.supports_lighting: raise NotSupportedByDevice() channel = channel.lower() mode = mode.lower() speed = speed.lower() direction = direction.lower() if mode == 'super': _LOGGER.warning('deprecated mode, move to super-fixed, super-breathing or super-wave') mode = 'super-fixed' if 'backwards' in mode: _LOGGER.warning('deprecated mode, move to direction=backwards option') mode = mode.replace('backwards-', '') direction = 'backward' mval, mod2, mod4, mincolors, maxcolors, ringonly = _COLOR_MODES[mode] if direction == 'backward': mod2 += 0x10 if ringonly and channel != 'ring': _LOGGER.warning('mode=%s unsupported with channel=%s, dropping to ring', mode, channel) channel = 'ring' steps = self._generate_steps(colors, mincolors, maxcolors, mode, ringonly) sval = _ANIMATION_SPEEDS[speed] byte2 = mod2 | _COLOR_CHANNELS[channel] for i, leds in enumerate(steps): seq = i << 5 byte4 = sval | seq | mod4 logo = [leds[0][1], leds[0][0], leds[0][2]] ring = list(itertools.chain(*leds[1:])) self._write([0x2, 0x4c, byte2, mval, byte4] + logo + ring) def _generate_steps(self, colors, mincolors, maxcolors, mode, ringonly): colors = list(colors) if len(colors) < mincolors: raise ValueError(f'not enough colors for mode={mode}, at least {mincolors} required') elif maxcolors == 0: if len(colors) > 0: _LOGGER.warning('too many colors for mode=%s, none needed', mode) colors = [(0, 0, 0)] # discard the input but ensure at least one step elif len(colors) > maxcolors: _LOGGER.warning('too many colors for mode=%s, dropping to %d', mode, maxcolors) colors = colors[:maxcolors] # generate steps from mode and colors: usually each color set by the user generates # one step, where it is specified to all leds and the device handles the animation; # but in super mode there is a single step and each color directly controls a led if 'super' not in mode: steps = [(color,)*9 for color in colors] elif ringonly: steps = [[(0, 0, 0)] + colors] else: steps = [colors] return steps def set_speed_profile(self, channel, profile, **kwargs): """Set channel to use a speed profile.""" if not self.supports_cooling_profiles: raise NotSupportedByDevice() norm = normalize_profile(profile, _CRITICAL_TEMPERATURE) # due to a firmware limitation the same set of temperatures must be # used on both channels; we reduce the number of writes by trimming the # interval and/or resolution to the most useful range stdtemps = list(range(20, 50)) + list(range(50, 60, 2)) + [60] interp = [(t, interpolate_profile(norm, t)) for t in stdtemps] cbase, dmin, dmax = _SPEED_CHANNELS[channel] for i, (temp, duty) in enumerate(interp): duty = clamp(duty, dmin, dmax) _LOGGER.info('setting %s PWM duty to %d%% for liquid temperature >= %d°C', channel, duty, temp) self._write([0x2, 0x4d, cbase + i, temp, duty]) def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed.""" if not self.supports_cooling: raise NotSupportedByDevice() elif self.supports_cooling_profiles: self.set_speed_profile(channel, [(0, duty), (59, duty), (60, 100), (100, 100)]) else: self.set_instantaneous_speed(channel, duty) def set_instantaneous_speed(self, channel, duty, **kwargs): """Set channel to speed, but do not ensure persistence.""" if not self.supports_cooling: raise NotSupportedByDevice() cbase, dmin, dmax = _SPEED_CHANNELS[channel] duty = clamp(duty, dmin, dmax) _LOGGER.info('setting %s PWM duty to %d%%', channel, duty) self._write([0x2, 0x4d, cbase & 0x70, 0, duty]) @property def supports_cooling_profiles(self): if self._supports_cooling_profiles is None: if self.supports_cooling: self._read(clear_first=False) self._supports_cooling_profiles = self._firmware_version >= (3, 0, 0) else: self._supports_cooling_profiles = False return self._supports_cooling_profiles def _read(self, clear_first=True): if clear_first: self.device.clear_enqueued_reports() msg = self.device.read(_READ_LENGTH) self._firmware_version = (msg[0xb], msg[0xc] << 8 | msg[0xd], msg[0xe]) return msg def _write(self, data): padding = [0x0]*(_WRITE_LENGTH - len(data)) self.device.write(data + padding) # deprecated aliases KrakenTwoDriver = Kraken2 liquidctl-1.5.1/liquidctl/driver/kraken3.py000066400000000000000000000375261401367561700207340ustar00rootroot00000000000000"""liquidctl drivers for fourth-generation NZXT Kraken X and Z liquid coolers. Supported devices: - NZXT Kraken X (X53, X63 and Z73) - NZXT Kraken Z (Z63 and Z73); no OLED screen control yet Copyright (C) 2020–2021 Tom Frey, Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import itertools from liquidctl.driver.usb import UsbHidDriver from liquidctl.util import normalize_profile, interpolate_profile, clamp, \ Hue2Accessory, HUE2_MAX_ACCESSORIES_IN_CHANNEL _LOGGER = logging.getLogger(__name__) _READ_LENGTH = 64 _WRITE_LENGTH = 64 _MAX_READ_ATTEMPTS = 12 # Available speed channels for model X coolers # name -> (channel_id, min_duty, max_duty) # TODO adjust min duty value to what the firmware enforces _SPEED_CHANNELS_KRAKENX = { 'pump': (0x1, 20, 100), } # Available speed channels for model Z coolers # name -> (channel_id, min_duty, max_duty) # TODO adjust min duty values to what the firmware enforces _SPEED_CHANNELS_KRAKENZ = { 'pump': (0x1, 20, 100), 'fan': (0x2, 0, 100), } _CRITICAL_TEMPERATURE = 59 # Available color channels and IDs for model X coolers _COLOR_CHANNELS_KRAKENX = { 'external': 0b001, 'ring': 0b010, 'logo': 0b100, 'sync': 0b111 } # Available color channels and IDs for model Z coolers _COLOR_CHANNELS_KRAKENZ = { 'external': 0b001, } # Available LED channel modes/animations # name -> (mode, size/variant, speed scale, min colors, max colors) # FIXME any point in a one-color *alternating* or tai-chi animations? # FIXME are all modes really supported by all channels? (this is better because # of synchronization, but it's not how the previous generation worked, so # I would like to double check) _COLOR_MODES = { 'off': (0x00, 0x00, 0, 0, 0), 'fixed': (0x00, 0x00, 0, 1, 1), 'fading': (0x01, 0x00, 1, 1, 8), 'super-fixed': (0x01, 0x01, 9, 1, 40), 'spectrum-wave': (0x02, 0x00, 2, 0, 0), 'marquee-3': (0x03, 0x03, 2, 1, 1), 'marquee-4': (0x03, 0x04, 2, 1, 1), 'marquee-5': (0x03, 0x05, 2, 1, 1), 'marquee-6': (0x03, 0x06, 2, 1, 1), 'covering-marquee': (0x04, 0x00, 2, 1, 8), 'alternating-3': (0x05, 0x03, 3, 1, 2), 'alternating-4': (0x05, 0x04, 3, 1, 2), 'alternating-5': (0x05, 0x05, 3, 1, 2), 'alternating-6': (0x05, 0x06, 3, 1, 2), 'moving-alternating-3': (0x05, 0x03, 4, 1, 2), 'moving-alternating-4': (0x05, 0x04, 4, 1, 2), 'moving-alternating-5': (0x05, 0x05, 4, 1, 2), 'moving-alternating-6': (0x05, 0x06, 4, 1, 2), 'pulse': (0x06, 0x00, 5, 1, 8), 'breathing': (0x07, 0x00, 6, 1, 8), 'super-breathing': (0x03, 0x00, 10, 1, 40), 'candle': (0x08, 0x00, 0, 1, 1), 'starry-night': (0x09, 0x00, 5, 1, 1), 'rainbow-flow': (0x0b, 0x00, 2, 0, 0), 'super-rainbow': (0x0c, 0x00, 2, 0, 0), 'rainbow-pulse': (0x0d, 0x00, 2, 0, 0), 'loading': (0x10, 0x00, 8, 1, 1), 'tai-chi': (0x0e, 0x00, 7, 1, 2), 'water-cooler': (0x0f, 0x00, 6, 2, 2), 'wings': (None, 0x00, 11, 1, 1), # deprecated in favor of direction=backward 'backwards-spectrum-wave': (0x02, 0x00, 2, 0, 0), 'backwards-marquee-3': (0x03, 0x03, 2, 1, 1), 'backwards-marquee-4': (0x03, 0x04, 2, 1, 1), 'backwards-marquee-5': (0x03, 0x05, 2, 1, 1), 'backwards-marquee-6': (0x03, 0x06, 2, 1, 1), 'covering-backwards-marquee': (0x04, 0x00, 2, 1, 8), 'backwards-moving-alternating-3': (0x05, 0x03, 4, 1, 2), 'backwards-moving-alternating-4': (0x05, 0x04, 4, 1, 2), 'backwards-moving-alternating-5': (0x05, 0x05, 4, 1, 2), 'backwards-moving-alternating-6': (0x05, 0x06, 4, 1, 2), 'backwards-rainbow-flow': (0x0b, 0x00, 2, 0, 0), 'backwards-super-rainbow': (0x0c, 0x00, 2, 0, 0), 'backwards-rainbow-pulse': (0x0d, 0x00, 2, 0, 0), } # A static value per channel that is somehow related to animation time and # synchronization, although the specific mechanism is not yet understood. # Could require information from `initialize`, but more testing is required. _STATIC_VALUE = { 0b001: 40, # may result in long all-off intervals (FIXME?) 0b010: 8, 0b100: 1, 0b111: 40, # may result in long all-off intervals (FIXME?) } # Speed scale/timing bytes # scale -> (slowest, slower, normal, faster, fastest) _SPEED_VALUE = { 0: ([0x32, 0x00], [0x32, 0x00], [0x32, 0x00], [0x32, 0x00], [0x32, 0x00]), 1: ([0x50, 0x00], [0x3c, 0x00], [0x28, 0x00], [0x14, 0x00], [0x0a, 0x00]), 2: ([0x5e, 0x01], [0x2c, 0x01], [0xfa, 0x00], [0x96, 0x00], [0x50, 0x00]), 3: ([0x40, 0x06], [0x14, 0x05], [0xe8, 0x03], [0x20, 0x03], [0x58, 0x02]), 4: ([0x20, 0x03], [0xbc, 0x02], [0xf4, 0x01], [0x90, 0x01], [0x2c, 0x01]), 5: ([0x19, 0x00], [0x14, 0x00], [0x0f, 0x00], [0x07, 0x00], [0x04, 0x00]), 6: ([0x28, 0x00], [0x1e, 0x00], [0x14, 0x00], [0x0a, 0x00], [0x04, 0x00]), 7: ([0x32, 0x00], [0x28, 0x00], [0x1e, 0x00], [0x14, 0x00], [0x0a, 0x00]), 8: ([0x14, 0x00], [0x14, 0x00], [0x14, 0x00], [0x14, 0x00], [0x14, 0x00]), 9: ([0x00, 0x00], [0x00, 0x00], [0x00, 0x00], [0x00, 0x00], [0x00, 0x00]), 10: ([0x37, 0x00], [0x28, 0x00], [0x19, 0x00], [0x0a, 0x00], [0x00, 0x00]), 11: ([0x6e, 0x00], [0x53, 0x00], [0x39, 0x00], [0x2e, 0x00], [0x20, 0x00]), } _ANIMATION_SPEEDS = { 'slowest': 0x0, 'slower': 0x1, 'normal': 0x2, 'faster': 0x3, 'fastest': 0x4, } class KrakenX3(UsbHidDriver): """Fourth-generation Kraken X liquid cooler.""" SUPPORTED_DEVICES = [ (0x1e71, 0x2007, None, 'NZXT Kraken X (X53, X63 or X73)', { 'speed_channels': _SPEED_CHANNELS_KRAKENX, 'color_channels': _COLOR_CHANNELS_KRAKENX, }) ] def __init__(self, device, description, speed_channels, color_channels, **kwargs): super().__init__(device, description) self._speed_channels = speed_channels self._color_channels = color_channels def initialize(self, **kwargs): """Initialize the device. Reports the current firmware of the device. Returns a list of (key, value, unit) tuples. """ self.device.clear_enqueued_reports() # request static infos self._write([0x10, 0x01]) # firmware info self._write([0x20, 0x03]) # lighting info # initialize update_interval = (lambda secs: 1 + round((secs - .5) / .25))(.5) # see issue #128 self._write([0x70, 0x02, 0x01, 0xb8, update_interval]) self._write([0x70, 0x01]) status = [] def parse_firm_info(msg): fw = f'{msg[0x11]}.{msg[0x12]}.{msg[0x13]}' status.append(('Firmware version', fw, '')) def parse_led_info(msg): channel_count = msg[14] assert channel_count == len(self._color_channels) - ('sync' in self._color_channels), \ f'Unexpected number of color channels received: {channel_count}' def find(channel, accessory): offset = 15 # offset of first channel/first accessory acc_id = msg[offset + channel * HUE2_MAX_ACCESSORIES_IN_CHANNEL + accessory] return Hue2Accessory(acc_id) if acc_id else None for i in range(HUE2_MAX_ACCESSORIES_IN_CHANNEL): accessory = find(0, i) if not accessory: break status.append((f'LED accessory {i + 1}', accessory, '')) if len(self._color_channels) > 1: found_ring = find(1, 0) == Hue2Accessory.KRAKENX_GEN4_RING found_logo = find(2, 0) == Hue2Accessory.KRAKENX_GEN4_LOGO status.append(('Pump Ring LEDs', 'detected' if found_ring else 'missing', '')) status.append(('Pump Logo LEDs', 'detected' if found_logo else 'missing', '')) assert found_ring and found_logo, "Pump ring and/or logo were not detected" self._read_until({b'\x11\x01': parse_firm_info, b'\x21\x03': parse_led_info}) return sorted(status) def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ self.device.clear_enqueued_reports() msg = self._read() if msg[15:17] == [0xff, 0xff]: _LOGGER.warning('unexpected temperature reading, possible firmware fault;') _LOGGER.warning('try resetting the device or updating the firmware') _LOGGER.warning('(see https://github.com/liquidctl/liquidctl/issues/172)') return [ ('Liquid temperature', msg[15] + msg[16] / 10, '°C'), ('Pump speed', msg[18] << 8 | msg[17], 'rpm'), ('Pump duty', msg[19], '%'), ] def set_color(self, channel, mode, colors, speed='normal', direction='forward', **kwargs): """Set the color mode for a specific channel.""" channel = channel.lower() mode = mode.lower() speed = speed.lower() direction = direction.lower() if 'backwards' in mode: _LOGGER.warning('deprecated mode, move to direction=backwards option') mode = mode.replace('backwards-', '') direction = 'backward' cid = self._color_channels[channel] _, _, _, mincolors, maxcolors = _COLOR_MODES[mode] colors = [[g, r, b] for [r, g, b] in colors] if len(colors) < mincolors: raise ValueError(f'not enough colors for mode={mode}, at least {mincolors} required') elif maxcolors == 0: if colors: _LOGGER.warning('too many colors for mode=%s, none needed', mode) colors = [[0, 0, 0]] # discard the input but ensure at least one step elif len(colors) > maxcolors: _LOGGER.warning('too many colors for mode=%s, dropping to %d', mode, maxcolors) colors = colors[:maxcolors] sval = _ANIMATION_SPEEDS[speed] self._write_colors(cid, mode, colors, sval, direction) def set_speed_profile(self, channel, profile, **kwargs): """Set channel to use a speed profile.""" cid, dmin, dmax = self._speed_channels[channel] header = [0x72, cid, 0x00, 0x00] norm = normalize_profile(profile, _CRITICAL_TEMPERATURE) stdtemps = list(range(20, _CRITICAL_TEMPERATURE + 1)) interp = [clamp(interpolate_profile(norm, t), dmin, dmax) for t in stdtemps] for temp, duty in zip(stdtemps, interp): _LOGGER.info('setting %s PWM duty to %d%% for liquid temperature >= %d°C', channel, duty, temp) self._write(header + interp) def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" self.set_speed_profile(channel, [(0, duty), (_CRITICAL_TEMPERATURE - 1, duty)]) def _read(self): data = self.device.read(_READ_LENGTH) return data def _read_until(self, parsers): for _ in range(_MAX_READ_ATTEMPTS): msg = self.device.read(_READ_LENGTH) prefix = bytes(msg[0:2]) func = parsers.pop(prefix, None) if func: func(msg) if not parsers: return assert False, f'missing messages (attempts={_MAX_READ_ATTEMPTS}, missing={len(parsers)})' def _write(self, data): padding = [0x0] * (_WRITE_LENGTH - len(data)) self.device.write(data + padding) def _write_colors(self, cid, mode, colors, sval, direction): mval, size_variant, speed_scale, mincolors, maxcolors = _COLOR_MODES[mode] color_count = len(colors) if 'super-fixed' == mode or 'super-breathing' == mode: color = list(itertools.chain(*colors)) + [0x00, 0x00, 0x00] * (maxcolors - color_count) speed_value = _SPEED_VALUE[speed_scale][sval] self._write([0x22, 0x10, cid, 0x00] + color) self._write([0x22, 0x11, cid, 0x00]) self._write([0x22, 0xa0, cid, 0x00, mval] + speed_value + [0x08, 0x00, 0x00, 0x80, 0x00, 0x32, 0x00, 0x00, 0x01]) elif mode == 'wings': # wings requires special handling self._write([0x22, 0x10, cid]) # clear out all independent LEDs self._write([0x22, 0x11, cid]) # clear out all independent LEDs color_lists = {} color_lists[0] = colors[0] * 2 color_lists[1] = [int(x // 2.5) for x in color_lists[0]] color_lists[2] = [int(x // 4) for x in color_lists[1]] color_lists[3] = [0x00] * 8 speed_value = _SPEED_VALUE[speed_scale][sval] for i in range(8): # send color scheme first, before enabling wings mode mod = 0x05 if i in [3, 7] else 0x01 alt = [0x04, 0x84] if i // 4 == 0 else [0x84, 0x04] msg = ([0x22, 0x20, cid, i, 0x04] + speed_value + [mod] + [0x00] * 7 + [0x02] + alt + [0x00] * 10) self._write(msg + color_lists[i % 4]) self._write([0x22, 0x03, cid, 0x08]) # this actually enables wings mode else: opcode = [0x2a, 0x04] address = [cid, cid] speed_value = _SPEED_VALUE[speed_scale][sval] header = opcode + address + [mval] + speed_value color = list(itertools.chain(*colors)) + [0, 0, 0] * (16 - color_count) if 'marquee' in mode: backwards_byte = 0x04 elif mode == 'starry-night' or 'moving-alternating' in mode: backwards_byte = 0x01 else: backwards_byte = 0x00 if direction == 'backward': backwards_byte += 0x02 if mode == 'fading' or mode == 'pulse' or mode == 'breathing': mode_related = 0x08 elif mode == 'tai-chi': mode_related = 0x05 elif mode == 'water-cooler': mode_related = 0x05 color_count = 0x01 elif mode == 'loading': mode_related = 0x04 else: mode_related = 0x00 static_byte = _STATIC_VALUE[cid] led_size = size_variant if mval == 0x03 or mval == 0x05 else 0x03 footer = [backwards_byte, color_count, mode_related, static_byte, led_size] self._write(header + color + footer) class KrakenZ3(KrakenX3): """Fourth-generation Kraken Z liquid cooler.""" SUPPORTED_DEVICES = [ (0x1e71, 0x3008, None, 'NZXT Kraken Z (Z63 or Z73) (experimental)', { 'speed_channels': _SPEED_CHANNELS_KRAKENZ, 'color_channels': _COLOR_CHANNELS_KRAKENZ, }) ] def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ self.device.clear_enqueued_reports() self._write([0x74, 0x01]) msg = self._read() return [ ('Liquid temperature', msg[15] + msg[16] / 10, '°C'), ('Pump speed', msg[18] << 8 | msg[17], 'rpm'), ('Pump duty', msg[19], '%'), ('Fan speed', msg[24] << 8 | msg[23], 'rpm'), ('Fan duty', msg[25], '%'), ] liquidctl-1.5.1/liquidctl/driver/nvidia.py000066400000000000000000000323001401367561700206310ustar00rootroot00000000000000"""liquidctl drivers for NVIDIA graphics cards. Copyright (C) 2020–2021 Jonas Malaco, Marshall Asch and contributors SPDX-License-Identifier: GPL-3.0-or-later """ from enum import Enum, unique import logging from liquidctl.driver.smbus import SmbusDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import check_unsafe _LOGGER = logging.getLogger(__name__) # vendor, devices NVIDIA = 0x10de NVIDIA_GTX_1080 = 0x1b80 NVIDIA_RTX_2080_TI_REV_A = 0x1e07 # https://www.nv-drivers.eu/nvidia-all-devices.html # https://pci-ids.ucw.cz/pci.ids # subsystem vendor ASUS, subsystem devices ASUS = 0x1043 ASUS_STRIX_RTX_2080_TI_OC = 0x866a # subsystem vendor EVGA, subsystem devices EVGA = 0x3842 EVGA_GTX_1080_FTW = 0x6286 @unique class _ModeEnum(bytes, Enum): def __new__(cls, value, required_colors): obj = bytes.__new__(cls, [value]) obj._value_ = value obj.required_colors = required_colors return obj def __str__(self): return self.name.capitalize() class EvgaPascal(SmbusDriver): """NVIDIA series 10 (Pascal) graphics card from EVGA.""" _REG_MODE = 0x0c _REG_RED = 0x09 _REG_GREEN = 0x0a _REG_BLUE = 0x0b _REG_PERSIST = 0x23 _PERSIST = 0xe5 @unique class Mode(_ModeEnum): OFF = (0x00, 0) FIXED = (0x01, 1) RAINBOW = (0x02, 0) BREATHING = (0x05, 1) @classmethod def probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): ADDRESS = 0x49 if (vendor and vendor != EVGA) \ or (address and int(address, base=16) != ADDRESS) \ or smbus.parent_subsystem_vendor != EVGA \ or smbus.parent_vendor != NVIDIA \ or smbus.parent_driver != 'nvidia' \ or release or serial: # will never match: always None return supported = [ (NVIDIA_GTX_1080, EVGA_GTX_1080_FTW, 'EVGA GTX 1080 FTW'), ] for (dev_id, sub_dev_id, desc) in supported: if (product and product != sub_dev_id) \ or (match and match.lower() not in desc.lower()) \ or smbus.parent_subsystem_device != sub_dev_id \ or smbus.parent_device != dev_id \ or not smbus.description.startswith('NVIDIA i2c adapter 1 '): continue dev = cls(smbus, desc, vendor_id=EVGA, product_id=EVGA_GTX_1080_FTW, address=ADDRESS) _LOGGER.debug('instanced driver for %s', desc) yield dev def get_status(self, verbose=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ # only RGB lighting information can be fetched for now; as that isn't # super interesting, only enable it in verbose mode if not verbose: return [] if not check_unsafe('smbus', **kwargs): _LOGGER.warning("%s: nothing to return, requires unsafe features 'smbus'", self.description) return [] mode = self.Mode(self._smbus.read_byte_data(self._address, self._REG_MODE)) status = [('Mode', mode, '')] if mode.required_colors > 0: r = self._smbus.read_byte_data(self._address, self._REG_RED) g = self._smbus.read_byte_data(self._address, self._REG_GREEN) b = self._smbus.read_byte_data(self._address, self._REG_BLUE) status.append(('Color', f'{r:02x}{g:02x}{b:02x}', '')) return status def set_color(self, channel, mode, colors, non_volatile=False, **kwargs): """Set the RGB lighting mode and, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Required colors | | -------- | --------- | --------------- | | led | off | 0 | | led | fixed | 1 | | led | breathing | 1 | | led | rainbow | 0 | The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `non_volatile=True`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. """ check_unsafe('smbus', error=True, **kwargs) colors = list(colors) try: mode = self.Mode[mode.upper()] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.required_colors: raise ValueError(f'{mode} mode requires {mode.required_colors} colors') if len(colors) > mode.required_colors: _LOGGER.debug('too many colors, dropping to %d', mode.required_colors) colors = colors[:mode.required_colors] self._smbus.write_byte_data(self._address, self._REG_MODE, mode.value) for r, g, b in colors: self._smbus.write_byte_data(self._address, self._REG_RED, r) self._smbus.write_byte_data(self._address, self._REG_GREEN, g) self._smbus.write_byte_data(self._address, self._REG_BLUE, b) if non_volatile: # the following write always fails, but nonetheless induces persistence try: self._smbus.write_byte_data(self._address, self._REG_PERSIST, self._PERSIST) except OSError as err: _LOGGER.debug('expected OSError when writing to _REG_PERSIST: %s', err) def initialize(self, **kwargs): """Initialize the device.""" pass def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() class RogTuring(SmbusDriver): """NVIDIA series 20 (Turing) graphics card from ASUS.""" _REG_RED = 0x04 _REG_GREEN = 0x05 _REG_BLUE = 0x06 _REG_MODE = 0x07 _REG_APPLY = 0x0e _SYNC_REG = 0x0c # unused _SENTINEL_ADDRESS = 0xffff # intentionally invalid _ASUS_GPU_APPLY_VAL = 0x01 @unique class Mode(_ModeEnum): OFF = (0x00, 0) # not a real mode; fixed is sent with RGB = 0 FIXED = (0x01, 1) BREATHING = (0x02, 1) FLASH = (0x03, 1) RAINBOW = (0x04, 0) @classmethod def probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): ADDRESSES = [0x29, 0x2a, 0x60] ASUS_GPU_MAGIC_VALUE = 0x1589 if (vendor and vendor != ASUS) \ or (address and int(address, base=16) not in ADDRESSES) \ or smbus.parent_subsystem_vendor != ASUS \ or smbus.parent_vendor != NVIDIA \ or smbus.parent_driver != 'nvidia' \ or release or serial: # will never match: always None return supported = [ (NVIDIA_RTX_2080_TI_REV_A, ASUS_STRIX_RTX_2080_TI_OC, 'ASUS Strix RTX 2080 Ti OC'), ] for (dev_id, sub_dev_id, desc) in supported: if (product and product != sub_dev_id) \ or (match and match.lower() not in desc.lower()) \ or smbus.parent_subsystem_device != sub_dev_id \ or smbus.parent_device != dev_id \ or not smbus.description.startswith('NVIDIA i2c adapter 1 '): continue selected_address = None if check_unsafe('smbus', **kwargs): for address in ADDRESSES: val1 = 0 val2 = 0 smbus.open() try: val1 = smbus.read_byte_data(address, 0x20) val2 = smbus.read_byte_data(address, 0x21) except: pass smbus.close() if val1 << 8 | val2 == ASUS_GPU_MAGIC_VALUE: selected_address = address break else: selected_address = cls._SENTINEL_ADDRESS _LOGGER.debug('unsafe features not enabled, using sentinel address') if selected_address is not None: dev = cls(smbus, desc, vendor_id=ASUS, product_id=dev_id, address=selected_address) _LOGGER.debug('instanced driver for %s at address %02x', desc, selected_address) yield dev def get_status(self, verbose=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ # only RGB lighting information can be fetched for now; as that isn't # super interesting, only enable it in verbose mode if not verbose: return [] if not check_unsafe('smbus', **kwargs): _LOGGER.warning("%s: nothing to return, requires unsafe features " "'smbus'", self.description) return [] assert self._address != self._SENTINEL_ADDRESS, \ 'invalid address (probing may not have had access to SMbus)' mode = self._smbus.read_byte_data(self._address, self._REG_MODE) red = self._smbus.read_byte_data(self._address, self._REG_RED) green = self._smbus.read_byte_data(self._address, self._REG_GREEN) blue = self._smbus.read_byte_data(self._address, self._REG_BLUE) # emulate `OFF` both ways if red == green == blue == 0: mode = 0 mode = self.Mode(mode) status = [('Mode', mode, '')] if mode.required_colors > 0: status.append(('Color', f'{red:02x}{green:02x}{blue:02x}', '')) return status def set_color(self, channel, mode, colors, non_volatile=False, **kwargs): """Set the lighting mode, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Required colors | | -------- | --------- | --------------- | | led | off | 0 | | led | fixed | 1 | | led | flash | 1 | | led | breathing | 1 | | led | rainbow | 0 | The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `non_volatile=True`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. """ check_unsafe('smbus', error=True, **kwargs) assert self._address != self._SENTINEL_ADDRESS, \ 'invalid address (probing may not have had access to SMbus)' colors = list(colors) try: mode = self.Mode[mode.upper()] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.required_colors: raise ValueError(f'{mode} mode requires {mode.required_colors} colors') if len(colors) > mode.required_colors: _LOGGER.debug('too many colors, dropping to %d', mode.required_colors) colors = colors[:mode.required_colors] if mode == self.Mode.OFF: self._smbus.write_byte_data(self._address, self._REG_MODE, self.Mode.FIXED.value) self._smbus.write_byte_data(self._address, self._REG_RED, 0x00) self._smbus.write_byte_data(self._address, self._REG_GREEN, 0x00) self._smbus.write_byte_data(self._address, self._REG_BLUE, 0x00) else: self._smbus.write_byte_data(self._address, self._REG_MODE, mode.value) for r, g, b in colors: self._smbus.write_byte_data(self._address, self._REG_RED, r) self._smbus.write_byte_data(self._address, self._REG_GREEN, g) self._smbus.write_byte_data(self._address, self._REG_BLUE, b) if non_volatile: self._smbus.write_byte_data(self._address, self._REG_APPLY, self._ASUS_GPU_APPLY_VAL) def initialize(self, **kwargs): """Initialize the device.""" pass def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() liquidctl-1.5.1/liquidctl/driver/nzxt_epsu.py000066400000000000000000000121661401367561700214260ustar00rootroot00000000000000"""liquidctl driver for NZXT E-series PSUs. Supported devices: NZXT E500, E650 and E850. Features: - electrical output monitoring: complete - general device monitoring: partial - fan control: missing - 12V multiple rail configuration: missing Copyright (C) 2019–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import time from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.pmbus import CommandCode as CMD from liquidctl.pmbus import linear_to_float _REPORT_LENGTH = 64 _MIN_DELAY = 0.0025 _ATTEMPTS = 3 _SEASONIC_READ_FIRMWARE_VERSION = CMD.MFR_SPECIFIC_FC _RAILS = ['+12V peripherals', '+12V EPS/ATX12V', '+12V motherboard/PCI-e', '+5V combined', '+3.3V combined'] class NzxtEPsu(UsbHidDriver): """NZXT E-series power supply unit.""" SUPPORTED_DEVICES = [ (0x7793, 0x5911, None, 'NZXT E500 (experimental)', {}), (0x7793, 0x5912, None, 'NZXT E650 (experimental)', {}), (0x7793, 0x2500, None, 'NZXT E850 (experimental)', {}), ] def initialize(self, **kwargs): """Initialize the device. Apparently not required. """ pass def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ self.device.clear_enqueued_reports() fw_human, fw_cam = self._get_fw_versions() status = [ ('Temperature', self._get_float(CMD.READ_TEMPERATURE_2), '°C'), ('Fan speed', self._get_float(CMD.READ_FAN_SPEED_1), 'rpm'), ('Firmware version', f'{fw_human}/{fw_cam}', ''), ] for i, name in enumerate(_RAILS): status.append((f'{name} output voltage', self._get_vout(i), 'V')) status.append((f'{name} output current', self._get_float(CMD.READ_IOUT, page=i), 'A')) status.append((f'{name} output power', self._get_float(CMD.READ_POUT, page=i), 'W')) return status def set_color(self, channel, mode, colors, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def _write(self, data): assert len(data) <= _REPORT_LENGTH packet = bytearray(1 + _REPORT_LENGTH) packet[1: 1 + len(data)] = data # device doesn't use numbered reports self.device.write(packet) def _read(self): return self.device.read(_REPORT_LENGTH) def _wait(self): """Give the device some time and avoid error responses. Not well understood but probably related to the PIC16F1455 microcontroller. It is possible that it isn't just used for a "dumb" PMBus/HID bridge, requiring time to be left for other tasks. """ time.sleep(_MIN_DELAY) def _exec_read(self, cmd, data_len): data = None msg = [0xad, 0, data_len + 1, 1, 0x60, cmd] for _ in range(_ATTEMPTS): self._wait() self._write(msg) res = self._read() # see comment in _exec_page_plus_read, but res[1] == 0xff has not # been seen in the wild yet # TODO replace with PEC byte check if res[0] == 0xaa and res[1] == data_len + 1: data = res break assert data, f'invalid response (attempts={_ATTEMPTS})' return data[2:(2 + data_len)] def _exec_page_plus_read(self, page, cmd, data_len): data = None msg = [0xad, 0, data_len + 2, 4, 0x60, CMD.PAGE_PLUS_READ, 2, page, cmd] for _ in range(_ATTEMPTS): self._wait() self._write(msg) res = self._read() # in captured traffic res[2] == 0xff appears to signal invalid data # (possibly due to the device being busy, see PMBus spec) # TODO replace with PEC byte check if res[0] == 0xaa and res[1] == data_len + 2 and res[2] == data_len: data = res break assert data, f'invalid response (attempts={_ATTEMPTS})' return data[3:(3 + data_len)] def _get_float(self, cmd, page=None): if page is None: return linear_to_float(self._exec_read(cmd, 2)) else: return linear_to_float(self._exec_page_plus_read(page, cmd, 2)) def _get_vout(self, rail): mode = self._exec_page_plus_read(rail, CMD.VOUT_MODE, 1)[0] assert mode >> 5 == 0 # assume vout_mode is always ulinear16 vout = self._exec_page_plus_read(rail, CMD.READ_VOUT, 2) return linear_to_float(vout, mode & 0x1f) def _get_fw_versions(self): minor, major = self._exec_read(_SEASONIC_READ_FIRMWARE_VERSION, 2) human_ver = f'{bytes([major]).decode()}{minor:03}' ascam_ver = int.from_bytes(bytes.fromhex(human_ver), byteorder='big') return (human_ver, ascam_ver) # deprecated aliases SeasonicEDriver = NzxtEPsu liquidctl-1.5.1/liquidctl/driver/rgb_fusion2.py000066400000000000000000000241331401367561700216030ustar00rootroot00000000000000"""liquidctl driver for Gigabyte RGB Fusion 2.0 USB controllers. Supported controllers: - ITE 5702: found in Gigabyte Z490 Vision D - ITE 8297: found in Gigabyte X570 Aorus Elite Copyright (C) 2020–2021 CaseySJ, Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ from collections import namedtuple import logging import sys from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import clamp _LOGGER = logging.getLogger(__name__) _USAGE_PAGE = 0xff89 _RGB_CONTROL_USAGE = 0xcc _OTHER_USAGE = 0x10 _REPORT_ID = 0xcc _REPORT_BYTE_LENGTH = 63 _INIT_CMD = 0x60 _COLOR_CHANNELS = { 'led1': (0x20, 0x01), 'led2': (0x21, 0x02), 'led3': (0x22, 0x04), 'led4': (0x23, 0x08), 'led5': (0x24, 0x10), 'led6': (0x25, 0x20), 'led7': (0x26, 0x40), 'led8': (0x27, 0x80), } _PULSE_SPEEDS = { 'slowest': (0x40, 0x06, 0x40, 0x06, 0x20, 0x03), 'slower': (0x78, 0x05, 0x78, 0x05, 0xbc, 0x02), 'normal': (0xb0, 0x04, 0xb0, 0x04, 0xf4, 0x01), 'faster': (0xe8, 0x03, 0xe8, 0x03, 0xf4, 0x01), 'fastest': (0x84, 0x03, 0x84, 0x03, 0xc2, 0x01), 'ludicrous': (0x20, 0x03, 0x20, 0x03, 0x90, 0x01), } _FLASH_SPEEDS = { 'slowest': (0x64, 0x00, 0x64, 0x00, 0x60, 0x09), 'slower': (0x64, 0x00, 0x64, 0x00, 0x90, 0x08), 'normal': (0x64, 0x00, 0x64, 0x00, 0xd0, 0x07), 'faster': (0x64, 0x00, 0x64, 0x00, 0x08, 0x07), 'fastest': (0x64, 0x00, 0x64, 0x00, 0x40, 0x06), 'ludicrous': (0x64, 0x00, 0x64, 0x00, 0x78, 0x05), } _DOUBLE_FLASH_SPEEDS = { 'slowest': (0x64, 0x00, 0x64, 0x00, 0x28, 0x0a), 'slower ': (0x64, 0x00, 0x64, 0x00, 0x60, 0x09), 'normal': (0x64, 0x00, 0x64, 0x00, 0x90, 0x08), 'faster': (0x64, 0x00, 0x64, 0x00, 0xd0, 0x07), 'fastest': (0x64, 0x00, 0x64, 0x00, 0x08, 0x07), 'ludicrous': (0x64, 0x00, 0x64, 0x00, 0x40, 0x06), } _COLOR_CYCLE_SPEEDS = { 'slowest': (0x78, 0x05, 0xb0, 0x04, 0x00, 0x00), 'slower': (0x7e, 0x04, 0x1a, 0x04, 0x00, 0x00), 'normal': (0x52, 0x03, 0xee, 0x02, 0x00, 0x00), 'faster': (0xf8, 0x02, 0x94, 0x02, 0x00, 0x00), 'fastest': (0x26, 0x02, 0xc2, 0x01, 0x00, 0x00), 'ludicrous': (0xcc, 0x01, 0x68, 0x01, 0x00, 0x00), } _ColorMode = namedtuple('_ColorMode', ['name', 'value', 'pulses', 'flash_count', 'cycle_count', 'max_brightness', 'takes_color', 'speed_values']) _COLOR_MODES = { mode.name: mode for mode in [ _ColorMode('off', 0x01, pulses=False, flash_count=0, cycle_count=0, max_brightness=0, takes_color=False, speed_values=None), _ColorMode('fixed', 0x01, pulses=False, flash_count=0, cycle_count=0, max_brightness=90, takes_color=True, speed_values=None), _ColorMode('pulse', 0x02, pulses=True, flash_count=0, cycle_count=0, max_brightness=90, takes_color=True, speed_values=_PULSE_SPEEDS), _ColorMode('flash', 0x03, pulses=True, flash_count=1, cycle_count=0, max_brightness=100, takes_color=True, speed_values=_FLASH_SPEEDS), _ColorMode('double-flash', 0x03, pulses=True, flash_count=2, cycle_count=0, max_brightness=100, takes_color=True, speed_values=_DOUBLE_FLASH_SPEEDS), _ColorMode('color-cycle', 0x04, pulses=False, flash_count=0, cycle_count=7, max_brightness=100, takes_color=False, speed_values=_COLOR_CYCLE_SPEEDS), ] } class RgbFusion2(UsbHidDriver): """liquidctl driver for Gigabyte RGB Fusion 2.0 USB controllers.""" SUPPORTED_DEVICES = [ (0x048d, 0x5702, None, 'Gigabyte RGB Fusion 2.0 5702 Controller', {}), (0x048d, 0x8297, None, 'Gigabyte RGB Fusion 2.0 8297 Controller', {}), ] @classmethod def probe(cls, handle, **kwargs): """Probe `handle` and yield corresponding driver instances. These devices have multiple top-level HID usages, and HidapiDevice handles matching other usages have to be ignored. """ # if usage_page/usage are not available due to hidapi limitations # (version, platform or backend), they are unfortunately left # uninitialized; because of this, we explicitly exclude the undesired # usage_page/usage pair, and assume in all other cases that we either # have the desired usage page/usage pair, or that on that system a # single handle is returned for that device interface (see: 259) if (handle.hidinfo['usage_page'] == _USAGE_PAGE and handle.hidinfo['usage'] == _OTHER_USAGE): return yield from super().probe(handle, **kwargs) def initialize(self, **kwargs): """Initialize the device. Returns a list of `(property, value, unit)` tuples, containing the firmware version and other useful information provided by the hardware. """ self._send_feature_report([_REPORT_ID, _INIT_CMD]) data = self._get_feature_report(_REPORT_ID) # be tolerant: 8297 controllers support report IDs yet return 0 in the # first byte, which is out of spec assert data[0] in (_REPORT_ID, 0) and data[1] == 0x01 null = data.index(0, 12) dev_name = str(bytes(data[12:null]), 'ascii', errors='replace') fw_version = tuple(data[4:8]) return [ ('Hardware name', dev_name, ''), ('Firmware version', '{}.{}.{}.{}'.format(*fw_version), ''), ] def get_status(self, **kwargs): """Get a status report. Currently returns an empty list, but this behavior is not guaranteed as in the future the device may start to report useful information. A non-empty list would contain `(property, value, unit)` tuples. """ _LOGGER.info('status reports not available from %s', self.description) return [] def set_color(self, channel, mode, colors, speed='normal', **kwargs): """Set the color mode for a specific channel. Up to eight individual channels are available, named 'led1' through 'led8'. In addition to these, the 'sync' channel can be used to apply the same settings to all channels. The table bellow summarizes the available channels. | Mode | Colors required | Speed is customizable | | ------------ | --------------- | --------------------- | | off | zero | no | | fixed | one | no | | pulse | one | yes | | flash | one | yes | | double-flash | one | yes | | color-cycle | zero | yes | `colors` should be an iterable of zero or one `[red, blue, green]` triples, where each red/blue/green component is a value in the range 0–255. `speed`, when supported by the `mode`, can be one of: `slowest`, `slower`, `normal` (default), `faster`, `fastest` or `ludicrous`. """ mode = _COLOR_MODES[mode.lower()] colors = iter(colors) channel = channel.lower() speed = speed.lower() if mode.takes_color: try: r, g, b = next(colors) single_color = (b, g, r) except StopIteration: raise ValueError(f'one color required for mode={mode.name}') from None else: single_color = (0, 0, 0) remaining = sum(1 for _ in colors) if remaining: _LOGGER.warning('too many colors for mode=%s, dropping %d', mode.name, remaining) brightness = clamp(100, 0, mode.max_brightness) # hardcode this for now data = [_REPORT_ID, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, mode.value, brightness, 0x00] data += single_color data += [0x00, 0x00, 0x00, 0x00, 0x00] if mode.speed_values: data += mode.speed_values[speed] else: data += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] data += [0x00, 0x00, mode.cycle_count, int(mode.pulses), mode.flash_count] if channel == 'sync': selected_channels = _COLOR_CHANNELS.values() else: selected_channels = (_COLOR_CHANNELS[channel],) for addr1, addr2 in selected_channels: data[1:3] = addr1, addr2 self._send_feature_report(data) self._execute_report() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def reset_all_channels(self): """Reset all LED channels.""" for addr1, _ in _COLOR_CHANNELS.values(): self._send_feature_report([_REPORT_ID, addr1, 0]) self._execute_report() def _get_feature_report(self, report_id): return self.device.get_feature_report(report_id, _REPORT_BYTE_LENGTH + 1) def _send_feature_report(self, data): padding = [0x0]*(_REPORT_BYTE_LENGTH + 1 - len(data)) self.device.send_feature_report(data + padding) def _execute_report(self): """Request for the previously sent lighting settings to be applied.""" self._send_feature_report([_REPORT_ID, 0x28, 0xff]) # Acknowledgments: # # Thanks to SgtSixPack for capturing USB traffic on 0x8297 and testing the driver on Windows. liquidctl-1.5.1/liquidctl/driver/smart_device.py000066400000000000000000000540471401367561700220400ustar00rootroot00000000000000"""liquidctl drivers for NZXT Smart Device V1/V2, Grid+ V3, HUE 2 and HUE 2 Ambient. Smart Device (V1) ----------------- The NZXT Smart Device is a fan and LED controller that ships with the H200i, H400i, H500i and H700i cases. It provides three independent fan channels with standard 4-pin connectors. Both PWM and DC control is supported, and the device automatically chooses the appropriate mode for each channel. Additionally, up to four chained HUE+ LED strips, or five Aer RGB fans, can be driven from only RGB channel available. The firmware installed on the device exposes several color presets, most of them common to other NZXT products. The device recognizes the type of accessory connected by measuring the resistance between the FD and GND lines.[1][2] In normal usage accessories should not be mixed. A microphone is also present onboard, for noise level optimization through CAM and AI. NZXT calls this feature Adaptive Noise Reduction (ANR). [1] https://forum.level1techs.com/t/nzxt-hue-a-look-inside/104836 [2] In parallel: 10 kOhm per HUE+ strip, 16 kOhm per Aer RGB fan. Grid+ V3 -------- The NZXT Grid+ V3 is a fan controller very similar to the Smart Device (V1). Comparing the two, the Grid+ has more fan channels (six in total), and no support for LEDs. Smart Device V2 --------------- The NZXT Smart Device V2 is a newer model of the original fan and LED controller. It ships with NZXT's cases released in mid-2019 including the H510 Elite, H510i, H710i, and H210i. It provides three independent fan channels with standard 4-pin connectors. Both PWM and DC control is supported, and the device automatically chooses the appropriate mode for each channel. Additionally, it features two independent lighting (Addressable RGB) channels, unlike the single channel in the original. NZXT Aer RGB 2 fans and HUE 2 lighting accessories (HUE 2 LED strip, HUE 2 Unerglow, HUE 2 Cable Comb) can be connected. The firmware installed on the device exposes several color presets, most of them common to other NZXT products. HUE 2 and HUE+ devices (including Aer RGB and Aer RGB 2 fans) are supported, but HUE 2 components cannot be mixed with HUE+ components in the same channel. Each lighting channel supports up to 6 accessories and a total of 40 LEDs. A microphone is still present onboard for noise level optimization through CAM and AI. RGB & Fan Controller -------------------- The NZXT RGB & Fan Controller is a retail version of the NZXT Smart Device V2. HUE 2 ----- The NZXT HUE 2 is an LED controller from the same generation of the Smart Device V2. The presets and limitations of the four LED channels are the same as in the Smart Device V2. HUE 2 Ambient ------------- HUE 2 Ambient is a variant of HUE 2 featuring 2 LED control channels. Driver ------ This driver implements all features available at the hardware level: - initialization - detection of connected fans and LED strips - control of fan speeds per channel - monitoring of fan presence, control mode, speed, voltage and current - control of lighting modes and colors - reporting of LED accessory count and type - monitoring of noise level (from the onboard microphone) - reporting of firmware version Software based features offered by CAM, like ANR, have not been implemented. After powering on from Mechanical Off, or if there have been hardware changes, the devices must be manually initialized by calling `initialize()`. This will cause all connected fans and LED accessories to be detected, and enable status updates. It is recommended to initialize the devices at every boot. Copyright (C) 2018–2021 Jonas Malaco, CaseySJ and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import clamp, Hue2Accessory, HUE2_MAX_ACCESSORIES_IN_CHANNEL _LOGGER = logging.getLogger(__name__) _ANIMATION_SPEEDS = { 'slowest': 0x0, 'slower': 0x1, 'normal': 0x2, 'faster': 0x3, 'fastest': 0x4, } _MIN_DUTY = 0 _MAX_DUTY = 100 class _CommonSmartDeviceDriver(UsbHidDriver): """Common functions of Smart Device and Grid drivers.""" def __init__(self, device, description, speed_channels, color_channels, **kwargs): """Instantiate a driver with a device handle.""" super().__init__(device, description) self._speed_channels = speed_channels self._color_channels = color_channels def set_color(self, channel, mode, colors, speed='normal', direction='forward', **kwargs): """Set the color mode. Only supported by Smart Device V1/V2 and HUE 2 controllers. """ if not self._color_channels: raise NotSupportedByDevice() channel = channel.lower() mode = mode.lower() speed = speed.lower() direction = direction.lower() if 'backwards' in mode: _LOGGER.warning('deprecated mode, move to direction=backwards option') mode = mode.replace('backwards-', '') direction = 'backward' cid = self._color_channels[channel] _, _, _, mincolors, maxcolors = self._COLOR_MODES[mode] colors = [[g, r, b] for [r, g, b] in colors] if len(colors) < mincolors: raise ValueError(f'not enough colors for mode={mode}, at least {mincolors} required') elif maxcolors == 0: if colors: _LOGGER.warning('too many colors for mode=%s, none needed', mode) colors = [[0, 0, 0]] # discard the input but ensure at least one step elif len(colors) > maxcolors: _LOGGER.warning('too many colors for mode=%s, dropping to %d', mode, maxcolors) colors = colors[:maxcolors] sval = _ANIMATION_SPEEDS[speed] self._write_colors(cid, mode, colors, sval, direction) def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed.""" if channel == 'sync': selected_channels = self._speed_channels else: selected_channels = {channel: self._speed_channels[channel]} for cname, (cid, dmin, dmax) in selected_channels.items(): duty = clamp(duty, dmin, dmax) _LOGGER.info('setting %s duty to %d%%', cname, duty) self._write_fixed_duty(cid, duty) def set_speed_profile(self, channel, profile, **kwargs): """Not Supported by this device.""" raise NotSupportedByDevice() def _write(self, data): padding = [0x0]*(self._WRITE_LENGTH - len(data)) self.device.write(data + padding) def _write_colors(self, cid, mode, colors, sval): raise NotImplementedError() def _write_fixed_duty(self, cid, duty): raise NotImplementedError() class SmartDevice(_CommonSmartDeviceDriver): """NZXT Smart Device (V1) or Grid+ V3.""" SUPPORTED_DEVICES = [ (0x1e71, 0x1714, None, 'NZXT Smart Device (V1)', { 'speed_channel_count': 3, 'color_channel_count': 1 }), (0x1e71, 0x1711, None, 'NZXT Grid+ V3', { 'speed_channel_count': 6, 'color_channel_count': 0 }), ] _READ_LENGTH = 21 _WRITE_LENGTH = 65 _COLOR_MODES = { # (byte2/mode, byte3/variant, byte4/size, min colors, max colors) 'off': (0x00, 0x00, 0x00, 0, 0), 'fixed': (0x00, 0x00, 0x00, 1, 1), 'super-fixed': (0x00, 0x00, 0x00, 1, 40), # independent leds 'fading': (0x01, 0x00, 0x00, 1, 8), 'spectrum-wave': (0x02, 0x00, 0x00, 0, 0), 'marquee-3': (0x03, 0x00, 0x00, 1, 1), 'marquee-4': (0x03, 0x00, 0x08, 1, 1), 'marquee-5': (0x03, 0x00, 0x10, 1, 1), 'marquee-6': (0x03, 0x00, 0x18, 1, 1), 'covering-marquee': (0x04, 0x00, 0x00, 1, 8), 'alternating': (0x05, 0x00, 0x00, 2, 2), 'moving-alternating': (0x05, 0x08, 0x00, 2, 2), 'pulse': (0x06, 0x00, 0x00, 1, 8), 'breathing': (0x07, 0x00, 0x00, 1, 8), # colors for each step 'super-breathing': (0x07, 0x00, 0x00, 1, 40), # one step, independent leds 'candle': (0x09, 0x00, 0x00, 1, 1), 'wings': (0x0c, 0x00, 0x00, 1, 1), 'super-wave': (0x0d, 0x00, 0x00, 1, 40), # independent ring leds # deprecated in favor of direction=backward 'backwards-spectrum-wave': (0x02, 0x00, 0x00, 0, 0), 'backwards-marquee-3': (0x03, 0x00, 0x00, 1, 1), 'backwards-marquee-4': (0x03, 0x00, 0x08, 1, 1), 'backwards-marquee-5': (0x03, 0x00, 0x10, 1, 1), 'backwards-marquee-6': (0x03, 0x00, 0x18, 1, 1), 'covering-backwards-marquee': (0x04, 0x00, 0x00, 1, 8), 'backwards-moving-alternating': (0x05, 0x08, 0x00, 2, 2), 'backwards-super-wave': (0x0d, 0x00, 0x00, 1, 40), } def __init__(self, device, description, speed_channel_count, color_channel_count, **kwargs): """Instantiate a driver with a device handle.""" speed_channels = {f'fan{i + 1}': (i, _MIN_DUTY, _MAX_DUTY) for i in range(speed_channel_count)} color_channels = {'led': (i) for i in range(color_channel_count)} super().__init__(device, description, speed_channels, color_channels, **kwargs) def initialize(self, **kwargs): """Initialize the device. Detects all connected fans and LED accessories, and allows subsequent calls to get_status. """ self._write([0x1, 0x5c]) # initialize/detect connected devices and their type self._write([0x1, 0x5d]) # start reporting def get_status(self, **kwargs): """Get a status report. Returns a list of (key, value, unit) tuples. """ status = [] noise = [] self.device.clear_enqueued_reports() for i, _ in enumerate(self._speed_channels): msg = self.device.read(self._READ_LENGTH) num = (msg[15] >> 4) + 1 state = msg[15] & 0x3 status.append((f'Fan {num}', ['—', 'DC', 'PWM'][state], '')) noise.append(msg[1]) if state: status.append((f'Fan {num} speed', msg[3] << 8 | msg[4], 'rpm')) status.append((f'Fan {num} voltage', msg[7] + msg[8]/100, 'V')) status.append((f'Fan {num} current', msg[10]/100, 'A')) if i != 0: continue fw = '{}.{}.{}'.format(msg[0xb], msg[0xc] << 8 | msg[0xd], msg[0xe]) status.append(('Firmware version', fw, '')) if self._color_channels: lcount = msg[0x11] status.append(('LED accessories', lcount, '')) if lcount > 0: ltype, lsize = [('HUE+ Strip', 10), ('Aer RGB', 8)][msg[0x10] >> 3] status.append(('LED accessory type', ltype, '')) status.append(('LED count (total)', lcount*lsize, '')) status.append(('Noise level', round(sum(noise)/len(noise)), 'dB')) return sorted(status) def _write_colors(self, cid, mode, colors, sval, direction='forward'): mval, mod3, mod4, _, _ = self._COLOR_MODES[mode] # generate steps from mode and colors: usually each color set by the user generates # one step, where it is specified to all leds and the device handles the animation; # but in super mode there is a single step and each color directly controls a led if direction == 'backward': mod3 += 0x10 if 'super' in mode: steps = [list(itertools.chain(*colors))] else: steps = [color*40 for color in colors] for i, leds in enumerate(steps): seq = i << 5 byte4 = sval | seq | mod4 self._write([0x2, 0x4b, mval, mod3, byte4] + leds[0:57]) self._write([0x3] + leds[57:]) def _write_fixed_duty(self, cid, duty): self._write([0x2, 0x4d, cid, 0, duty]) class SmartDevice2(_CommonSmartDeviceDriver): """NZXT HUE 2 lighting and, optionally, fan controller.""" SUPPORTED_DEVICES = [ (0x1e71, 0x2006, None, 'NZXT Smart Device V2', { 'speed_channel_count': 3, 'color_channel_count': 2 }), (0x1e71, 0x2001, None, 'NZXT HUE 2', { 'speed_channel_count': 0, 'color_channel_count': 4 }), (0x1e71, 0x2002, None, 'NZXT HUE 2 Ambient', { 'speed_channel_count': 0, 'color_channel_count': 2 }), (0x1e71, 0x2009, None, 'NZXT RGB & Fan Controller', { 'speed_channel_count': 3, 'color_channel_count': 2 }), ] _MAX_READ_ATTEMPTS = 12 _READ_LENGTH = 64 _WRITE_LENGTH = 64 _COLOR_MODES = { # (mode, size/variant, moving, min colors, max colors) 'off': (0x00, 0x00, 0x00, 0, 0), 'fixed': (0x00, 0x00, 0x00, 1, 1), 'super-fixed': (0x01, 0x00, 0x00, 1, 40), # independent leds 'fading': (0x01, 0x00, 0x00, 1, 8), 'spectrum-wave': (0x02, 0x00, 0x00, 0, 0), 'marquee-3': (0x03, 0x00, 0x00, 1, 1), 'marquee-4': (0x03, 0x01, 0x00, 1, 1), 'marquee-5': (0x03, 0x02, 0x00, 1, 1), 'marquee-6': (0x03, 0x03, 0x00, 1, 1), 'covering-marquee': (0x04, 0x00, 0x00, 1, 8), 'alternating-3': (0x05, 0x00, 0x00, 2, 2), 'alternating-4': (0x05, 0x01, 0x00, 2, 2), 'alternating-5': (0x05, 0x02, 0x00, 2, 2), 'alternating-6': (0x05, 0x03, 0x00, 2, 2), 'moving-alternating-3': (0x05, 0x00, 0x10, 2, 2), # byte4: 0x10 = moving 'moving-alternating-4': (0x05, 0x01, 0x10, 2, 2), # byte4: 0x10 = moving 'moving-alternating-5': (0x05, 0x02, 0x10, 2, 2), # byte4: 0x10 = moving 'moving-alternating-6': (0x05, 0x03, 0x10, 2, 2), # byte4: 0x10 = moving 'pulse': (0x06, 0x00, 0x00, 1, 8), 'breathing': (0x07, 0x00, 0x00, 1, 8), # colors for each step 'super-breathing': (0x03, 0x19, 0x00, 1, 40), # independent leds 'candle': (0x08, 0x00, 0x00, 1, 1), 'starry-night': (0x09, 0x00, 0x00, 1, 1), 'rainbow-flow': (0x0b, 0x00, 0x00, 0, 0), 'super-rainbow': (0x0c, 0x00, 0x00, 0, 0), 'rainbow-pulse': (0x0d, 0x00, 0x00, 0, 0), 'wings': (None, 0x00, 0x00, 1, 1), # wings requires special handling # deprecated in favor of direction=backward 'backwards-spectrum-wave': (0x02, 0x00, 0x00, 0, 0), 'backwards-marquee-3': (0x03, 0x00, 0x00, 1, 1), 'backwards-marquee-4': (0x03, 0x01, 0x00, 1, 1), 'backwards-marquee-5': (0x03, 0x02, 0x00, 1, 1), 'backwards-marquee-6': (0x03, 0x03, 0x00, 1, 1), 'covering-backwards-marquee': (0x04, 0x00, 0x00, 1, 8), 'backwards-moving-alternating-3': (0x05, 0x00, 0x01, 2, 2), 'backwards-moving-alternating-4': (0x05, 0x01, 0x01, 2, 2), 'backwards-moving-alternating-5': (0x05, 0x02, 0x01, 2, 2), 'backwards-moving-alternating-6': (0x05, 0x03, 0x01, 2, 2), 'backwards-rainbow-flow': (0x0b, 0x00, 0x00, 0, 0), 'backwards-super-rainbow': (0x0c, 0x00, 0x00, 0, 0), 'backwards-rainbow-pulse': (0x0d, 0x00, 0x00, 0, 0), } def __init__(self, device, description, speed_channel_count, color_channel_count, **kwargs): """Instantiate a driver with a device handle.""" speed_channels = {f'fan{i + 1}': (i, _MIN_DUTY, _MAX_DUTY) for i in range(speed_channel_count)} color_channels = {f'led{i + 1}': (1 << i) for i in range(color_channel_count)} color_channels['sync'] = (1 << color_channel_count) - 1 super().__init__(device, description, speed_channels, color_channels, **kwargs) def initialize(self, **kwargs): """Initialize the device. Detects and reports all connected fans and LED accessories, and allows subsequent calls to get_status. Returns a list of (key, value, unit) tuples. """ self.device.clear_enqueued_reports() # initialize update_interval = (lambda secs: 1 + round((secs - .5) / .25))(.5) # see issue #128 self._write([0x60, 0x02, 0x01, 0xe8, update_interval, 0x01, 0xe8, update_interval]) self._write([0x60, 0x03]) # request static infos self._write([0x10, 0x01]) # firmware info self._write([0x20, 0x03]) # lighting info status = [] def parse_firm_info(msg): fw = f'{msg[0x11]}.{msg[0x12]}.{msg[0x13]}' status.append(('Firmware version', fw, '')) def parse_led_info(msg): channel_count = msg[14] offset = 15 # offset of first channel/first accessory for c in range(channel_count): for a in range(HUE2_MAX_ACCESSORIES_IN_CHANNEL): accessory_id = msg[offset + c * HUE2_MAX_ACCESSORIES_IN_CHANNEL + a] if accessory_id == 0: break status.append((f'LED {c + 1} accessory {a + 1}', Hue2Accessory(accessory_id), '')) self._read_until({b'\x11\x01': parse_firm_info, b'\x21\x03': parse_led_info}) return sorted(status) def get_status(self, **kwargs): """Get a status report. Returns a list of (key, value, unit) tuples. """ if not self._speed_channels: return [] status = [] def parse_fan_info(msg): rpm_offset = 24 duty_offset = 40 noise_offset = 56 for i, _ in enumerate(self._speed_channels): if ((msg[rpm_offset] != 0x0) and (msg[rpm_offset + 1] != 0x0)): status.append((f'Fan {i + 1} speed', msg[rpm_offset + 1] << 8 | msg[rpm_offset], 'rpm')) status.append((f'Fan {i + 1} duty', msg[duty_offset + i], '%')) rpm_offset += 2 status.append(('Noise level', msg[noise_offset], 'dB')) self.device.clear_enqueued_reports() self._read_until({b'\x67\x02': parse_fan_info}) return sorted(status) def _read_until(self, parsers): for _ in range(self._MAX_READ_ATTEMPTS): msg = self.device.read(self._READ_LENGTH) prefix = bytes(msg[0:2]) func = parsers.pop(prefix, None) if func: func(msg) if not parsers: return assert False, f'missing messages (attempts={self._MAX_READ_ATTEMPTS}, missing={len(parsers)})' def _write_colors(self, cid, mode, colors, sval, direction='forward',): mval, mod3, movingFlag, mincolors, maxcolors = self._COLOR_MODES[mode] color_count = len(colors) if maxcolors == 40: led_padding = [0x00, 0x00, 0x00]*(maxcolors - color_count) # turn off remaining LEDs leds = list(itertools.chain(*colors)) + led_padding self._write([0x22, 0x10, cid, 0x00] + leds[0:60]) # send first 20 colors to device (3 bytes per color) self._write([0x22, 0x11, cid, 0x00] + leds[60:]) # send remaining colors to device self._write([0x22, 0xa0, cid, 0x00, mval, mod3, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x32, 0x00, 0x00, 0x01]) elif mode == 'wings': # wings requires special handling for [g, r, b] in colors: self._write([0x22, 0x10, cid]) # clear out all independent LEDs self._write([0x22, 0x11, cid]) # clear out all independent LEDs color_lists = [] * 3 color_lists[0] = [g, r, b] * 8 color_lists[1] = [int(x // 2.5) for x in color_lists[0]] color_lists[2] = [int(x // 4) for x in color_lists[1]] for i in range(8): # send color scheme first, before enabling wings mode mod = 0x05 if i in [3, 7] else 0x01 msg = ([0x22, 0x20, cid, i, 0x04, 0x39, 0x00, mod, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x05, 0x85, 0x05, 0x85, 0x05, 0x85, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) self._write(msg + color_lists[i % 4]) self._write([0x22, 0x03, cid, 0x08]) # this actually enables wings mode else: byte7 = movingFlag # sets 'moving' flag for moving alternating modes byte8 = direction == 'backward' # sets 'backwards' flag byte9 = mod3 if mval == 0x03 else color_count # specifies 'marquee' LED size byte10 = mod3 if mval == 0x05 else 0x00 # specifies LED size for 'alternating' modes header = [0x28, 0x03, cid, 0x00, mval, sval, byte7, byte8, byte9, byte10] self._write(header + list(itertools.chain(*colors))) def _write_fixed_duty(self, cid, duty): msg = [0x62, 0x01, 0x01 << cid, 0x00, 0x00, 0x00] # fan channel passed as bitflag in last 3 bits of 3rd byte msg[cid + 3] = duty # duty percent in 4th, 5th, and 6th bytes for, respectively, fan1, fan2 and fan3 self._write(msg) # backwards compatibility NzxtSmartDeviceDriver = SmartDevice SmartDeviceDriver = SmartDevice SmartDeviceV2Driver = SmartDevice2 liquidctl-1.5.1/liquidctl/driver/smbus.py000066400000000000000000000276471401367561700205320ustar00rootroot00000000000000"""Base SMBus bus and driver APIs. For now, these are unstable APIs, and only Linux is supported. Copyright (C) 2020–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ from collections import namedtuple from pathlib import Path import logging import os import sys from liquidctl.driver.base import BaseDriver, BaseBus, find_all_subclasses from liquidctl.util import check_unsafe, LazyHexRepr _LOGGER = logging.getLogger(__name__) if sys.platform == 'linux': # WARNING: the tests rely on being able to override which SMBus # implementation is used here; this is done through the SMBus attribute # created below, do not move/replace/change it, nor access it during module # initialization from smbus import SMBus LinuxEeprom = namedtuple('LinuxEeprom', 'name data') class LinuxI2c(BaseBus): """The Linux I²C (`/sys/bus/i2c`) bus.""" def __init__(self, i2c_root='/sys/bus/i2c'): self._i2c_root = Path(i2c_root) def find_devices(self, bus=None, usb_port=None, **kwargs): """Find compatible SMBus devices.""" if usb_port: # a usb_port filter implies an USB bus return devices = self._i2c_root.joinpath('devices') if not devices.exists(): _LOGGER.debug('skipping %s, %s not available', self.__class__.__name__, devices) return drivers = sorted(find_all_subclasses(SmbusDriver), key=lambda x: (x.__module__, x.__name__)) _LOGGER.debug('searching %s (%s)', self.__class__.__name__, ', '.join(map(lambda x: x.__name__, drivers))) for i2c_dev in devices.iterdir(): try: i2c_bus = LinuxI2cBus(i2c_dev) except ValueError as err: _LOGGER.debug('ignoring %s, %s', i2c_dev.name, err) continue if bus and bus != i2c_bus.name: continue _LOGGER.debug('found I²C bus %s', i2c_bus.name) yield from i2c_bus.find_devices(drivers, **kwargs) class LinuxI2cBus: """A Linux I²C device, which is itself an I²C bus. Should not be instantiated directly; use `LinuxI2c.find_devices` instead. This type mimics the `smbus.SMBus` read/write/close APIs. However, `open` does not take any parameters, and not all APIs are available. """ # note: this is not a liquidctl BaseBus, as that would cause # find_liquidctl_devices to try to directly instantiate it def __init__(self, i2c_dev): self._i2c_dev = i2c_dev self._smbus = None try: assert i2c_dev.name.startswith('i2c-') self._number = int(i2c_dev.name[4:]) except: raise ValueError('cannot infer bus number') def find_devices(self, drivers, **kwargs): """Probe drivers and find compatible devices in this bus.""" for drv in drivers: yield from drv.probe(self, **kwargs) def open(self): """Open the I²C bus.""" if not self._smbus: try: self._smbus = SMBus(self._number) except FileNotFoundError: if Path('/sys/class/i2c-dev').exists(): raise raise OSError('kernel module i2c-dev not loaded') from None def read_byte(self, address): """Read a single byte from a device.""" value = self._smbus.read_byte(address) _LOGGER.debug('read byte @ 0x%02x:0x%02x', address, value) return value def read_byte_data(self, address, register): """Read a single byte from a designated register.""" value = self._smbus.read_byte_data(address, register) _LOGGER.debug('read byte data @ 0x%02x:0x%02x 0x%02x', address, register, value) return value def read_word_data(self, address, register): """Read a single 2-byte word from a given register.""" value = self._smbus.read_word_data(address, register) _LOGGER.debug('read word data @ 0x%02x:0x%02x 0x%02x', address, register, value) return value def read_block_data(self, address, register): """Read a block of up to 32 bytes from a given register.""" data = self._smbus.read_block_data(address, register) _LOGGER.debug('read block data @ 0x%02x:0x%02x: %r', address, register, LazyHexRepr(data)) return data def write_byte(self, address, value): """Write a single byte to a device.""" _LOGGER.debug('writing byte @ 0x%02x: 0x%02x', address, value) return self._smbus.write_byte(address, value) def write_byte_data(self, address, register, value): """Write a single byte to a designated register.""" _LOGGER.debug('writing byte data @ 0x%02x:0x%02x 0x%02x', address, register, value) return self._smbus.write_byte_data(address, register, value) def write_word_data(self, address, register, value): """Write a single 2-byte word to a designated register.""" _LOGGER.debug('writing word data @ 0x%02x:0x%02x 0x%02x', address, register, value) return self._smbus.write_word_data(address, register, value) def write_block_data(self, address, register, data): """Write a block of byte data to a given register.""" _LOGGER.debug('writing block data @ 0x%02x:0x%02x %r', address, register, LazyHexRepr(data)) return self._smbus.write_block_data(address, register, data) def close(self): """Close the I²C connection.""" if self._smbus: self._smbus.close() self._smbus = None def load_eeprom(self, address): """Return EEPROM name and data in `address`, or None if N/A.""" # uses kernel facilities to avoid directly reading from the EEPROM # or managing its pages, also avoiding the need for unsafe=smbus dev = f'{self._number}-{address:04x}' try: name = self._i2c_dev.joinpath(dev, 'name').read_text().strip() eeprom = self._i2c_dev.joinpath(dev, 'eeprom').read_bytes() return LinuxEeprom(name, eeprom) except Exception: return None @property def name(self): return self._i2c_dev.name @property def description(self): return self._try_sysfs_read('name') @property def parent_vendor(self): return self._try_sysfs_read_hex('device/vendor') @property def parent_device(self): return self._try_sysfs_read_hex('device/device') @property def parent_subsystem_vendor(self): return self._try_sysfs_read_hex('device/subsystem_vendor') @property def parent_subsystem_device(self): return self._try_sysfs_read_hex('device/subsystem_device') @property def parent_driver(self): try: return Path(os.readlink(self._i2c_dev.joinpath('device/driver'))).name except FileNotFoundError: return None def __str__(self): if self.description: return f'{self.name}: {self.description}' return self.name def __repr__(self): def hexid(maybe): if maybe is not None: return f'{maybe:#06x}' return 'None' return f'{self.__class__.__name__}: name: {self.name!r}, description:' \ f' {self.description!r}, parent_vendor: {hexid(self.parent_vendor)},' \ f' parent_device: {hexid(self.parent_device)}, parent_subsystem_vendor:' \ f' {hexid(self.parent_subsystem_vendor)},' \ f' parent_subsystem_device: {hexid(self.parent_subsystem_device)},' \ f' parent_driver: {self.parent_driver!r}' def _try_sysfs_read(self, *sub, default=None): try: return self._i2c_dev.joinpath(*sub).read_text().rstrip() except FileNotFoundError: return default def _try_sysfs_read_hex(self, *sub, default=None): try: return int(self._i2c_dev.joinpath(*sub).read_text(), base=16) except FileNotFoundError: return default class SmbusDriver(BaseDriver): """Base driver class for SMBus devices.""" @classmethod def probe(cls, smbus, **kwargs): raise NotImplementedError() @classmethod def find_supported_devices(cls, root_bus=None, **kwargs): """Find devices specifically compatible with this driver.""" if sys.platform != 'linux': return [] if not root_bus: root_bus = LinuxI2c() devs = filter(lambda x: isinstance(x, cls), root_bus.find_devices(**kwargs)) return list(devs) def __init__(self, smbus, description, vendor_id=None, product_id=None, address=None, **kwargs): # note: vendor_id and product_id are liquidctl properties intended to # allow the user to differentiate and ultimately filter devices; in the # context of SMBus, drivers may choose to use the parent's PCI # **subsystem** vendor/device IDs for this task, as those are more # specific and closer to the product the user purchased than the less # specific PCI vendor/device IDs. assert address is not None self._smbus = smbus self._description = description self._vendor_id = vendor_id self._product_id = product_id self._address = address def connect(self, **kwargs): """Connect to the device.""" if check_unsafe('smbus', **kwargs): self._smbus.open() else: # do not raise: some driver APIs may not access the bus after all, # and allowing the device to pseudo-connect is convenient given the # current API structure; APIs that do access the bus should check # for the 'smbus' feature themselves and, if necessary, raise # UnsafeFeaturesNotEnabled(*requirements) # (see also: check_unsafe(..., error=True)) _LOGGER.debug("SMBus is disabled, missing unsafe feature 'smbus'") return self def disconnect(self, **kwargs): """Disconnect from the device.""" self._smbus.close() @property def description(self): """Human readable description of the corresponding device.""" return self._description @property def vendor_id(self): """Numeric vendor identifier, or None if N/A.""" return self._vendor_id @property def product_id(self): """Numeric product identifier, or None if N/A.""" return self._product_id @property def release_number(self): """Device versioning number, or None if N/A. In USB devices this is bcdDevice. """ return None @property def serial_number(self): """Serial number reported by the device, or None if N/A.""" return None @property def bus(self): """Bus the device is connected to, or None if N/A.""" return self._smbus.name @property def address(self): """Address of the device on the corresponding bus, or None if N/A.""" return f'{self._address:#04x}' @property def port(self): """Physical location of the device, or None if N/A.""" return None liquidctl-1.5.1/liquidctl/driver/usb.py000066400000000000000000000470421401367561700201610ustar00rootroot00000000000000"""Base USB bus, driver and device APIs. This modules provides abstractions over several platform and implementation differences. As such, there is a lot of boilerplate here, but callers should be able to disregard almost everything and simply work on the UsbDriver/ UsbHidDriver level. BaseUsbDriver └── device: PyUsbDevice ├── uses PyUSB └── backed by (in order of priority) ├── libusb-1.0 ├── libusb-0.1 └── OpenUSB UsbHidDriver ├── extends: BaseUsbDriver └── device: HidapiDevice ├── uses hidapi └── backed by ├── hid.dll on Windows ├── hidraw on Linux if it was enabled during the build of hidapi ├── IOHidManager on MacOS └── libusb-1.0 on all other cases UsbDriver ├── extends: BaseUsbDriver └── allows to differentiate between UsbHidDriver and (non HID) UsbDriver UsbDriver and UsbHidDriver are meant to be used as base classes to the actual device drivers. The users of those drivers generally do not care about read, write or other low level operations; thus, these low level operations are placed in .device. However, there still are legitimate reasons as to why someone would want to directly access the lower layers (device wrapper level, device implementation level, or lower). We do not hide or mark those references as private, but good judgement should be exercised when calling anything within .device. The USB drivers are organized into two buses. The recommended way to initialize and bind drivers is through their respective buses, though .find_supported_devices can also be useful in certain scenarios. HidapiBus └── drivers: all (recursive) subclasses of UsbHidDriver PyUsbBus └── drivers: all (recursive) subclasses of UsbDriver The subclass constructor can generally be kept unaware of the implementation details of the device parameter, and find_supported_devices already accepts keyword arguments and forwards them to the driver constructor. Copyright (C) 2019–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import sys import usb try: import hidraw as hid except ModuleNotFoundError: import hid from liquidctl.driver.base import BaseDriver, BaseBus, find_all_subclasses from liquidctl.util import LazyHexRepr _LOGGER = logging.getLogger(__name__) class BaseUsbDriver(BaseDriver): """Base driver class for generic USB devices. Each driver should provide its own list of SUPPORTED_DEVICES, as well as implementations for all methods applicable to the devices is supports. SUPPORTED_DEVICES should consist of a list of (vendor id, product id, None (reserved), description, and extra kwargs) tuples. find_supported_devices will pass these extra kwargs, as well as any it receives, to the constructor. """ SUPPORTED_DEVICES = [] @classmethod def probe(cls, handle, vendor=None, product=None, release=None, serial=None, match=None, **kwargs): """Probe `handle` and yield corresponding driver instances.""" for vid, pid, _, description, devargs in cls.SUPPORTED_DEVICES: if (vendor and vendor != vid) or handle.vendor_id != vid: continue if (product and product != pid) or handle.product_id != pid: continue if release and handle.release_number != release: continue if serial and handle.serial_number != serial: continue if match and match.lower() not in description.lower(): continue consargs = devargs.copy() consargs.update(kwargs) dev = cls(handle, description, **consargs) _LOGGER.debug('instanced driver for %s', description) yield dev def __init__(self, device, description, **kwargs): self.device = device self._description = description def connect(self, **kwargs): """Connect to the device.""" self.device.open() return self def disconnect(self, **kwargs): """Disconnect from the device.""" self.device.close() @property def description(self): """Human readable description of the corresponding device.""" return self._description @property def vendor_id(self): """16-bit numeric vendor identifier.""" return self.device.vendor_id @property def product_id(self): """16-bit umeric product identifier.""" return self.device.product_id @property def release_number(self): """16-bit BCD device versioning number.""" return self.device.release_number @property def serial_number(self): """Serial number reported by the device, or None if N/A.""" return self.device.serial_number @property def bus(self): """Bus the device is connected to, or None if N/A.""" return self.device.bus @property def address(self): """Address of the device on the corresponding bus, or None if N/A. Dependendent on bus enumeration order. """ return self.device.address @property def port(self): """Physical location of the device, or None if N/A. Tuple of USB port numbers, from the root hub to this device. Not dependendent on bus enumeration order. """ return self.device.port class UsbHidDriver(BaseUsbDriver): """Base driver class for USB Human Interface Devices (HIDs).""" @classmethod def find_supported_devices(cls, **kwargs): """Find devices specifically compatible with this driver.""" devs = [] for vid, pid, _, _, _ in cls.SUPPORTED_DEVICES: for dev in HidapiBus().find_devices(vendor=vid, product=pid, **kwargs): if type(dev) == cls: devs.append(dev) return devs def __init__(self, device, description, **kwargs): # compatibility with v1.1.0 drivers, which could be directly # instantiated with a usb.core.Device if isinstance(device, usb.core.Device): clname = self.__class__.__name__ _LOGGER.warning('constructing a %s instance from a usb.core.Device has been deprecated, ' 'use %s.find_supported_devices() or pass a HidapiDevice handle', clname, clname) usbdev = device hidinfo = next(info for info in hid.enumerate(usbdev.idVendor, usbdev.idProduct) if info['serial_number'] == usbdev.serial_number) assert hidinfo, 'Could not find device in HID bus' device = HidapiDevice(hid, hidinfo) super().__init__(device, description, **kwargs) class UsbDriver(BaseUsbDriver): """Base driver class for regular USB devices. Specifically, regular USB devices are *not* Human Interface Devices (HIDs). """ @classmethod def find_supported_devices(cls, **kwargs): """Find devices specifically compatible with this driver.""" devs = [] for vid, pid, _, _, _ in cls.SUPPORTED_DEVICES: for dev in PyUsbBus().find_devices(vendor=vid, product=pid, **kwargs): if type(dev) == cls: devs.append(dev) return devs class PyUsbDevice: """"A PyUSB backed device. PyUSB will automatically pick the first available backend (at runtime). The supported backends are: - libusb-1.0 - libusb-0.1 - OpenUSB """ def __init__(self, usbdev, bInterfaceNumber=None): self.api = usb self.usbdev = usbdev self.bInterfaceNumber = bInterfaceNumber self._attached = False def _select_interface(self, cfg): return self.bInterfaceNumber or 0 def open(self, bInterfaceNumber=0): """Connect to the device. Ensure the device is configured and replace the kernel kernel on the selected interface, if necessary. """ # we assume the device is already configured, there is only one # configuration, or the first one is desired try: cfg = self.usbdev.get_active_configuration() except usb.core.USBError as err: if err.args[0] == 'Configuration not set': _LOGGER.debug('setting the (first) configuration') self.usbdev.set_configuration() # FIXME device or handle might not be ready for use yet cfg = self.usbdev.get_active_configuration() else: raise self.bInterfaceNumber = self._select_interface(cfg) _LOGGER.debug('selected interface: %d', self.bInterfaceNumber) if (sys.platform.startswith('linux') and self.usbdev.is_kernel_driver_active(self.bInterfaceNumber)): _LOGGER.debug('replacing stock kernel driver with libusb') self.usbdev.detach_kernel_driver(self.bInterfaceNumber) self._attached = True def claim(self): """Explicitly claim the device from other programs.""" _LOGGER.debug('explicitly claim interface') usb.util.claim_interface(self.usbdev, self.bInterfaceNumber) def release(self): """Release the device to other programs.""" if sys.platform == 'win32': # on Windows we need to release the entire device for other # programs to be able to access it _LOGGER.debug('explicitly release device') usb.util.dispose_resources(self.usbdev) else: # on Linux, and possibly on Mac and BSDs, releasing the specific # interface is enough _LOGGER.debug('explicitly release interface') usb.util.release_interface(self.usbdev, self.bInterfaceNumber) def close(self): """Disconnect from the device. Clean up and (Linux only) reattach the kernel driver. """ self.release() if self._attached: _LOGGER.debug('restoring stock kernel driver') self.usbdev.attach_kernel_driver(self.bInterfaceNumber) self._attached = False def read(self, endpoint, length, timeout=None): """Read from endpoint.""" data = self.usbdev.read(endpoint, length, timeout=timeout) _LOGGER.debug('read %d bytes: %r', len(data), LazyHexRepr(data)) return data def write(self, endpoint, data, timeout=None): """Write to endpoint.""" _LOGGER.debug('writting %d bytes: %r', len(data), LazyHexRepr(data)) return self.usbdev.write(endpoint, data, timeout=timeout) def ctrl_transfer(self, *args, **kwargs): """Submit a contrl transfer.""" _LOGGER.debug('sending control transfer with %r, %r', args, kwargs) return self.usbdev.ctrl_transfer(*args, **kwargs) @classmethod def enumerate(cls, vid=None, pid=None): args = {} if vid: args['idVendor'] = vid if pid: args['idProduct'] = pid for handle in usb.core.find(find_all=True, **args): yield cls(handle) @property def vendor_id(self): return self.usbdev.idVendor @property def product_id(self): return self.usbdev.idProduct @property def release_number(self): return self.usbdev.bcdDevice @property def serial_number(self): return self.usbdev.serial_number @property def bus(self): return f'usb{self.usbdev.bus}' # follow Linux model @property def address(self): return self.usbdev.address @property def port(self): return self.usbdev.port_numbers def __eq__(self, other): return type(self) == type(other) and self.bus == other.bus and self.address == other.address class HidapiDevice: """A hidapi backed device. Depending on the platform, the selected `hidapi` and how it was built, this might use any of the following backends: - hid.dll on Windows - hidraw on Linux, if it was enabled during the build of hidapi - IOHidManager on MacOS - libusb-1.0 on all other cases The default hidapi API is the module 'hid'. On standard Linux builds of the hidapi package, this might default to a libusb-1.0 backed implementation; at the same time an alternate 'hidraw' module may also be provided. The latter is prefered, when available. Note: if a libusb-backed 'hid' is used on Linux (assuming default build options) it will detach the kernel driver, making hidraw and hwmon unavailable for that device. To fix, rebind the device to usbhid with: echo '-:1.0' | sudo tee /sys/bus/usb/drivers/usbhid/bind """ def __init__(self, hidapi, hidapi_dev_info): self.api = hidapi self.hidinfo = hidapi_dev_info self.hiddev = self.api.device() def open(self): """Connect to the device.""" self.hiddev.open_path(self.hidinfo['path']) def close(self): """NOOP.""" self.hiddev.close() def clear_enqueued_reports(self): """Clear already enqueued incoming reports. The OS generally enqueues incomming reports for open HIDs, and hidapi emulates this when running on top of libusb. On Linux, up to 64 reports can be enqueued. This method quickly reads and discards any already enqueued reports, and is useful when later reads are not expected to return stale data. """ if self.hiddev.set_nonblocking(True) == 0: timeout_ms = 0 # use hid_read; wont block because call succeeded else: timeout_ms = 1 # smallest timeout forwarded to hid_read_timeout discarded = 0 while self.hiddev.read(max_length=1, timeout_ms=timeout_ms): discarded += 1 _LOGGER.debug('discarded %d previously enqueued reports', discarded) def read(self, length): """Read raw report from HID. The returned data follows the semantics of the Linux HIDRAW API. > On a device which uses numbered reports, the first byte of the > returned data will be the report number; the report data follows, > beginning in the second byte. For devices which do not use numbered > reports, the report data will begin at the first byte. """ self.hiddev.set_nonblocking(False) data = self.hiddev.read(length) _LOGGER.debug('read %d bytes: %r', len(data), LazyHexRepr(data)) return data def write(self, data): """Write raw report to HID. The buffer should follow the semantics of the Linux HIDRAW API. > The first byte of the buffer passed to write() should be set to the > report number. If the device does not use numbered reports, the > first byte should be set to 0. The report data itself should begin > at the second byte. """ _LOGGER.debug('writting report 0x%02x with %d bytes: %r', data[0], len(data) - 1, LazyHexRepr(data, start=1)) res = self.hiddev.write(data) if res < 0: raise OSError('Could not write to device') if res != len(data): _LOGGER.debug('wrote %d total bytes, expected %d', res, len(data)) return res def get_feature_report(self, report_id, length): """Get feature report that matches `report_id` from HID. If the device does not use numbered reports, set `report_id` to 0. Unlike `read`, the returned data follows semantics similar to `write` and `send_feature_report`: the first byte will always contain the report ID (or 0), and the report data itself will being at the second byte. """ data = self.hiddev.get_feature_report(report_id, length) _LOGGER.debug('got feature report 0x%02x with %d bytes: %r', data[0], len(data) - 1, LazyHexRepr(data, start=1)) return data def send_feature_report(self, data): """Send feature report to HID. The buffer should follow the semantics of `write`. > The first byte of the buffer passed to write() should be set to the > report number. If the device does not use numbered reports, the > first byte should be set to 0. The report data itself should begin > at the second byte. """ _LOGGER.debug('sending feature report 0x%02x with %d bytes: %r', data[0], len(data) - 1, LazyHexRepr(data, start=1)) res = self.hiddev.send_feature_report(data) if res < 0: raise OSError('Could not send feature report to device') if res != len(data): _LOGGER.debug('sent %d total bytes, expected %d', res, len(data)) return res @classmethod def enumerate(cls, api, vid=None, pid=None): infos = api.enumerate(vid or 0, pid or 0) if sys.platform == 'darwin': infos = sorted(infos, key=lambda info: info['path']) for info in infos: yield cls(api, info) @property def vendor_id(self): return self.hidinfo['vendor_id'] @property def product_id(self): return self.hidinfo['product_id'] @property def release_number(self): return self.hidinfo['release_number'] @property def serial_number(self): return self.hidinfo['serial_number'] @property def bus(self): return 'hid' # follow Linux model @property def address(self): return self.hidinfo['path'].decode(errors='replace') @property def port(self): return None def __eq__(self, other): return type(self) == type(other) and self.bus == other.bus and self.address == other.address class HidapiBus(BaseBus): def find_devices(self, vendor=None, product=None, bus=None, address=None, usb_port=None, **kwargs): """Find compatible HID devices.""" handles = HidapiDevice.enumerate(hid, vendor, product) drivers = sorted(find_all_subclasses(UsbHidDriver), key=lambda x: (x.__module__, x.__name__)) _LOGGER.debug('searching %s (%s)', self.__class__.__name__, ', '.join(map(lambda x: x.__name__, drivers))) for handle in handles: if bus and handle.bus != bus: continue if address and handle.address != address: continue if usb_port and handle.port != usb_port: continue _LOGGER.debug('found HID device %04x:%04x', handle.vendor_id, handle.product_id) for drv in drivers: yield from drv.probe(handle, vendor=vendor, product=product, **kwargs) class PyUsbBus(BaseBus): def find_devices(self, vendor=None, product=None, bus=None, address=None, usb_port=None, **kwargs): """ Find compatible regular USB devices.""" drivers = sorted(find_all_subclasses(UsbDriver), key=lambda x: (x.__module__, x.__name__)) _LOGGER.debug('searching %s (%s)', self.__class__.__name__, ', '.join(map(lambda x: x.__name__, drivers))) for handle in PyUsbDevice.enumerate(vendor, product): if bus and handle.bus != bus: continue if address and handle.address != address: continue if usb_port and handle.port != usb_port: continue _LOGGER.debug('found USB device %04x:%04x', handle.vendor_id, handle.product_id) for drv in drivers: yield from drv.probe(handle, vendor=vendor, product=product, **kwargs) liquidctl-1.5.1/liquidctl/error.py000066400000000000000000000005731401367561700172240ustar00rootroot00000000000000"""Standardized liquidctl errors. Copyright (C) 2020–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ class ExpectationNotMet(Exception): """Unstable.""" pass class NotSupportedByDevice(Exception): pass class NotSupportedByDriver(Exception): pass class UnsafeFeaturesNotEnabled(Exception): """Unstable.""" pass liquidctl-1.5.1/liquidctl/keyval.py000066400000000000000000000106341401367561700173650ustar00rootroot00000000000000"""Simple key-value based storage for liquidctl drivers. Copyright (C) 2019–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import os import sys import tempfile from ast import literal_eval _LOGGER = logging.getLogger(__name__) XDG_RUNTIME_DIR = os.getenv('XDG_RUNTIME_DIR') def get_runtime_dirs(appname='liquidctl'): """Return base directories for application runtime data. Directories are returned in order of preference. """ if sys.platform == 'win32': dirs = [os.path.join(os.getenv('TEMP'), appname)] elif sys.platform == 'darwin': dirs = [os.path.expanduser(os.path.join('~/Library/Caches', appname))] elif sys.platform == 'linux': # threat all other platforms as *nix and conform to XDG basedir spec dirs = [] if XDG_RUNTIME_DIR: dirs.append(os.path.join(XDG_RUNTIME_DIR, appname)) # regardless whether XDG_RUNTIME_DIR is set, fallback to /var/run if it # is available; this allows a user with XDG_RUNTIME_DIR set to still # find data stored by another user as long as it is in the fallback # path (see #37 for a real world use case) if os.path.isdir('/var/run'): dirs.append(os.path.join('/var/run', appname)) assert dirs, 'Could not get a suitable place to store runtime data' else: dirs = [os.path.join('/tmp', appname)] return dirs class _FilesystemBackend: def _sanitize(self, key): if not isinstance(key, str): raise TypeError('key must str') if not key.isidentifier(): raise ValueError('key must be valid Python identifier') return key def __init__(self, key_prefixes, runtime_dirs=get_runtime_dirs()): key_prefixes = [self._sanitize(p) for p in key_prefixes] # compute read and write dirs from base runtime dirs: the first base # dir is selected for writes and prefered for reads self._read_dirs = [os.path.join(x, *key_prefixes) for x in runtime_dirs] self._write_dir = self._read_dirs[0] os.makedirs(self._write_dir, exist_ok=True) if sys.platform == 'linux': # set the sticky bit to prevent removal during cleanup os.chmod(self._write_dir, 0o1700) _LOGGER.debug('data in %s', self._write_dir) def load(self, key): for base in self._read_dirs: path = os.path.join(base, key) if not os.path.isfile(path): continue try: with open(path, mode='r') as f: data = f.read().strip() if len(data) == 0: value = None else: value = literal_eval(data) _LOGGER.debug('loaded %s=%r (from %s)', key, value, path) except OSError as err: _LOGGER.warning('%s exists but could not be read: %s', path, err) except ValueError as err: _LOGGER.warning('%s exists but was corrupted: %s', key, err) else: return value _LOGGER.debug('no data (file) found for %s', key) return None def store(self, key, value): data = repr(value) assert literal_eval(data) == value, 'encode/decode roundtrip fails' path = os.path.join(self._write_dir, key) fd, tmp = tempfile.mkstemp(dir=self._write_dir, text=True) with open(fd, mode='w') as f: f.write(data) f.flush() os.replace(tmp, path) _LOGGER.debug('stored %s=%r (in %s)', key, value, path) class RuntimeStorage: """Unstable API.""" def __init__(self, key_prefixes): self._backend = _FilesystemBackend(key_prefixes) def load(self, key, of_type=None, default=None): """Unstable API.""" value = self._backend.load(key) if value is None: return default elif of_type and not isinstance(value, of_type): return default else: return value def store(self, key, value): """Unstable API.""" self._backend.store(key, value) return value def load_int(self, key, default=None): """Unstable API. Soon to be removed.""" return self.load(key, of_type=int, default=default) def store_int(self, key, value): """Unstable API. Soon to be removed.""" self.store(key, value) liquidctl-1.5.1/liquidctl/pmbus.py000066400000000000000000000135461401367561700172250ustar00rootroot00000000000000"""Constants and methods for interfacing with PMBus compliant devices. Specifications: - Power Systems Management Protocol Specification. Revision 1.3.1, 2015. Available uppon request, check the PMBus website. - Power Systems Management Protocol Specification. Revision 1.2, 2010. Available on the PMBus website. http://pmbus.org/Assets/PDFS/Public/PMBus_Specification_Part_I_Rev_1-2_20100906.pdf http://pmbus.org/Assets/PDFS/Public/PMBus_Specification_Part_II_Rev_1-2_20100906.pdf - System Management Bus (SMBus) Specification. Version 3.1, 2018. Available on the SMBus website. http://smbus.org/specs/SMBus_3_1_20180319.pdf Additional references: - Milios, John. CRC-8 firmware implementations for SMBus. 1999. http://sbs-forum.org/marcom/dc2/20_crc-8_firmware_implementations.pdf - Pircher, Thomas. pycrc -- parameterisable CRC calculation utility and C source code generator: CRC algorithms implemented in Python. https://github.com/tpircher/pycrc/blob/master/pycrc/algorithms.py - White, Robert V. Using the PMBus Protocol. 2005. http://pmbus.org/Assets/Present/Using_The_PMBus_20051012.pdf Copyright (C) 2019–2019 Jonas Malaco and contributors Includes a CRC-8 implementation adapted from pycrc by Thomas Pircher. Copyright (c) 2006-2017 Thomas Pircher SPDX-License-Identifier: GPL-3.0-or-later """ import math from enum import IntEnum, IntFlag, unique @unique class WriteBit(IntFlag): WRITE = 0x00 READ = 0x01 @unique class CommandCode(IntEnum): """Incomplete enumeration of the PMBus command codes.""" PAGE = 0x00 CLEAR_FAULTS = 0x03 PAGE_PLUS_WRITE = 0x05 PAGE_PLUS_READ = 0x06 VOUT_MODE = 0x20 FAN_CONFIG_1_2 = 0x3a FAN_COMMAND_1 = 0x3b FAN_COMMAND_2 = 0x3c FAN_CONFIG_3_4 = 0x3d FAN_COMMAND_3 = 0x3e FAN_COMMAND_4 = 0x3f READ_EIN = 0x86 READ_EOUT = 0x87 READ_VIN = 0x88 READ_IIN = 0x89 READ_VCAP = 0x8a READ_VOUT = 0x8b READ_IOUT = 0x8c READ_TEMPERATURE_1 = 0x8d READ_TEMPERATURE_2 = 0x8e READ_TEMPERATURE_3 = 0x8f READ_FAN_SPEED_1 = 0x90 READ_FAN_SPEED_2 = 0x91 READ_FAN_SPEED_3 = 0x92 READ_FAN_SPEED_4 = 0x93 READ_DUTY_CYCLE = 0x94 READ_FREQUENCY = 0x95 READ_POUT = 0x96 READ_PIN = 0x97 READ_PMBUS_REVISON = 0x98 MFR_ID = 0x99 MFR_MODEL = 0x9a MFR_REVISION = 0x9b MFR_LOCATION = 0x9c MFR_DATE = 0x9d MFR_SERIAL = 0x9e MFR_SPECIFIC_D1 = 0xd1 MFR_SPECIFIC_D2 = 0xd2 MFR_SPECIFIC_D8 = 0xd8 MFR_SPECIFIC_DC = 0xdc MFR_SPECIFIC_EE = 0xee MFR_SPECIFIC_F0 = 0xf0 MFR_SPECIFIC_FC = 0xfc def linear_to_float(bytes, vout_exp=None): """Read PMBus LINEAR11 and ULINEAR16 numeric values. If `vout_exp` is None the value is interpreted as a 2 byte LINEAR11 value. The mantissa is stored in the lower 11 bits, in two's-complement, and the exponent is is stored in the upper 5 bits, also in two's-complement. Otherwise the value is assumed to be encoded in ULINEAR16, where the exponent is read from the lower 5 bits of `vout_exp` (which is assumed to be the output from VOUT_MOE) and the mantissa is the unsigned 2 byte integer in `bytes`. Per the SMBus specification, the lowest order byte is sent first (endianess is little). >>> linear_to_float(bytes.fromhex('67e3')) 54.4375 >>> linear_to_float(bytes.fromhex('6703'), vout_exp=0x1c) 54.4375 """ tmp = int.from_bytes(bytes[:2], byteorder='little') if vout_exp is None: exp = tmp >> 11 fra = tmp & 0x7ff if fra > 1023: fra = fra - 2048 else: exp = vout_exp & 0x1f fra = tmp if exp > 15: exp = exp - 32 return fra * 2**exp def float_to_linear11(float): """Encode float in PMBus LINEAR11 format. A LINEAR11 number is a 2 byte value with an 11 bit two's complement mantissa and a 5 bit two's complement exponent. Per the SMBus specification, the lowest order byte is sent first (endianess is little). >>> float_to_linear11(3.3).hex() '4dc3' >>> float_to_linear11(0.0).hex() '0000' >>> linear_to_float(float_to_linear11(2812)) 2812 >>> linear_to_float(float_to_linear11(-2812)) -2812 """ if float == 0: return b'\x00\x00' max_y = 1023 n = math.ceil(math.log(math.fabs(float)/max_y, 2)) y = round(float * 2**(-n)) if n < 0: n = n + 32 if y < 0: y = y + 2048 return int.to_bytes((n << 11) | y, length=2, byteorder='little') def compute_pec(bytes): """ Compute a 8-bit Packet Error Code (PEC) for `bytes`. According to the SMBus specification, the PEC is computed using a 8-bit cyclic rendundancy check (CRC-8) with the polynominal x⁸ + x² + x¹ + x⁰. The computation uses a 256-byte lookup table. Based on https://github.com/tpircher/pycrc/blob/master/pycrc/algorithms.py. >>> hex(compute_pec(bytes('123456789', 'ascii'))) '0xf4' >>> hex(compute_pec(bytes.fromhex('5c'))) '0x93' >>> hex(compute_pec(bytes.fromhex('5c93'))) '0x0' """ tbl = _gen_pec_table() reg = 0 for octet in bytes: idx = reg ^ octet reg = tbl[idx] return reg def _gen_pec_table(): """Generate the lookup table for compute_pec. Once a table is generated it is reused for all subsequent calls. """ global _PEC_TBL if _PEC_TBL: return _PEC_TBL tbl = [0 for i in range(_PEC_TBL_LEN)] for i in range(_PEC_TBL_LEN): reg = i for _ in range(8): if reg & _PEC_MSB_MASK != 0: reg = (reg << 1) ^ _PEC_POLY else: reg = (reg << 1) tbl[i] = reg & _PEC_MASK _PEC_TBL = tbl return tbl _PEC_WIDTH = 8 _PEC_MSB_MASK = 1 << (_PEC_WIDTH - 1) _PEC_MASK = (_PEC_MSB_MASK << 1) - 1 _PEC_POLY = (0b100000111 & _PEC_MASK) _PEC_TBL_LEN = 256 _PEC_TBL = None liquidctl-1.5.1/liquidctl/util.py000066400000000000000000000262261401367561700170530ustar00rootroot00000000000000"""Assorted utilities used by drivers and the CLI. Copyright (C) 2018–2021 Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import colorsys import logging from ast import literal_eval from enum import Enum, unique from liquidctl.error import UnsafeFeaturesNotEnabled _LOGGER = logging.getLogger(__name__) HUE2_MAX_ACCESSORIES_IN_CHANNEL = 6 @unique class Hue2Accessory(Enum): """Mapping of HUE 2 accessory IDs and names. >>> Hue2Accessory(4) >>> str(Hue2Accessory(4)) 'HUE 2 LED Strip 300 mm' Unknown IDs are automatically translated to equivalent pseudo-names. >>> Hue2Accessory(59) >>> Hue2Accessory(59).value == Hue2Accessory(59).value True >>> Hue2Accessory(59) != Hue2Accessory(58) True """ HUE_PLUS_LED_STRIP = (0x01, 'HUE+ LED Strip') AER_RGB1_FAN = (0x02, 'AER RGB 1') HUE2_LED_STRIP_300 = (0x04, 'HUE 2 LED Strip 300 mm') HUE2_LED_STRIP_250 = (0x05, 'HUE 2 LED Strip 250 mm') HUE2_LED_STRIP_200 = (0x06, 'HUE 2 LED Strip 200 mm') HUE2_CABLE_COMB = (0x07, 'HUE 2 Cable Comb') HUE2_UNDERGLOW_300 = (0x09, 'HUE 2 Underglow 300 mm') HUE2_UNDERGLOW_200 = (0x0a, 'HUE 2 Underglow 200 mm') AER_RGB2_120 = (0x0b, 'AER RGB 2 120 mm') AER_RGB2_140 = (0x0c, 'AER RGB 2 140 mm') KRAKENX_GEN4_RING = (0x10, 'Kraken X (X53, X63 or X73) Pump Ring') KRAKENX_GEN4_LOGO = (0x11, 'Kraken X (X53, X63 or X73) Pump Logo') def __new__(cls, value, pretty_name): member = object.__new__(cls) member.pretty_name = pretty_name member._value_ = value return member @classmethod def _missing_(cls, value): dummy = object.__new__(cls) dummy.pretty_name = 'Unknown' dummy._name_ = f'UNKNOWN_{value}' dummy._value_ = value return dummy def __str__(self): return self.pretty_name def __eq__(self, other): return self.value == other.value class LazyHexRepr: """Wrap an indexed collection of bytes with a lazy hex __repr__. This is useful for logging, which uses `%` string formatting to lazily generate the messages, only when needed. >>> '%r' % LazyHexRepr(b'abc') '61:62:63' Start and end indices may also be specified. >>> '%r' % LazyHexRepr(b'abc', start=1) '62:63' >>> '%r' % LazyHexRepr(b'abc', end=-1) '61:62' """ def __init__(self, data, start=None, end=None, sep=':'): self.data = data self.start = start self.end = end self.sep = sep def __repr__(self): hexvals = map(lambda x: f'{x:02x}', self.data[self.start: self.end]) return self.sep.join(hexvals) def rpadlist(list, width, fillitem=0): """Pad `list` with `fillitem` to `width`. >>> rpadlist([1, 2, 3], 5) [1, 2, 3, 0, 0] >>> rpadlist([1, 2, 3], 5, fillitem=None) [1, 2, 3, None, None] """ pad_width = width - len(list) list.extend([fillitem] * pad_width) return list def clamp(value, clampmin, clampmax): """Clamp numeric `value` to interval [`clampmin`, `clampmax`].""" clamped = max(clampmin, min(clampmax, value)) if clamped != value: _LOGGER.debug('clamped %s to interval [%s, %s]', value, clampmin, clampmax) return clamped def fraction_of_byte(ratio=None, percentage=None): """Return `ratio` xor `percentage` expressed as a fraction of 255. >>> fraction_of_byte(ratio=.8) 204 >>> fraction_of_byte(percentage=20) 51 """ if percentage is not None: ratio = percentage / 100 if ratio is not None: if ratio < 0 or ratio > 1: raise ValueError('cannot express ratios outside of [0, 1]') return round(ratio * 255) raise ValueError('either ratio or percentage must not be None') def u16le_from(buffer, offset=0): """Read an unsigned 16-bit little-endian integer from `buffer`. >>> u16le_from(b'\x45\x05\x03') 1349 >>> u16le_from(b'\x45\x05\x03', offset=1) 773 """ return int.from_bytes(buffer[offset: offset + 2], byteorder='little') def u16be_from(buffer, offset=0): """Read an unsigned 16-bit big-endian integer from `buffer`. >>> u16be_from(b'\x45\x05\x03') 17669 >>> u16be_from(b'\x45\x05\x03', offset=1) 1283 """ return int.from_bytes(buffer[offset: offset + 2], byteorder='big') def delta(profile): """Compute a profile's Δx and Δy.""" return [(cur[0]-prev[0], cur[1]-prev[1]) for cur, prev in zip(profile[1:], profile[:-1])] def normalize_profile(profile, critx, max_value=100): """Normalize a [(x:int, y:int), ...] profile. The normalized profile will ensure that: - the profile is a monotonically increasing function (i.e. for every i, i > 1, x[i] - x[i-1] > 0 and y[i] - y[i-1] >= 0) - the profile is sorted - a (critx, 100) failsafe is enforced - only the first point that sets y := 100 is kept >>> normalize_profile([(30, 40), (25, 25), (35, 30), (40, 35), (40, 80)], 60) [(25, 25), (30, 40), (35, 40), (40, 80), (60, 100)] >>> normalize_profile([(30, 40), (25, 25), (35, 30), (40, 100)], 60) [(25, 25), (30, 40), (35, 40), (40, 100)] >>> normalize_profile([(30, 40), (25, 25), (35, 100), (40, 100)], 60) [(25, 25), (30, 40), (35, 100)] >>> normalize_profile([], 60) [(60, 100)] >>> normalize_profile([], 60, 300) [(60, 300)] """ profile = sorted(list(profile) + [(critx, max_value)], key=lambda p: (p[0], -p[1])) mono = profile[0:1] for (x, y), (xb, yb) in zip(profile[1:], profile[:-1]): if x == xb: continue if y < yb: y = yb mono.append((x, y)) if y == max_value: break return mono def interpolate_profile(profile, x): """Interpolate y given x and a [(x: int, y: int), ...] profile. Requires the profile to be sorted by x, with no duplicate x values (see normalize_profile). Expects profiles with integer x and y values, and returns duty rounded to the nearest integer. >>> interpolate_profile([(20, 50), (50, 70), (60, 100)], 33) 59 >>> interpolate_profile([(20, 50), (50, 70)], 19) 50 >>> interpolate_profile([(20, 50), (50, 70)], 51) 70 >>> interpolate_profile([(20, 50)], 20) 50 """ lower, upper = profile[0], profile[-1] for step in profile: if step[0] <= x: lower = step if step[0] >= x: upper = step break if lower[0] == upper[0]: return lower[1] return round(lower[1] + (x - lower[0])/(upper[0] - lower[0])*(upper[1] - lower[1])) def color_from_str(x): """Parse a color, and, if necessary, translate it into the RGB model. The input string can be encoded in several formats: - ffffff: hexadecimal RGB implicit tuple (with or without the prefix '0x') - rgb(255, 255, 255): explicit RGB, R,G,B ∊ [0, 255] - hsv(360, 100, 100): explicit HSV, H ∊ [0, 360], SV ∊ [0, 100] - hsl(360, 100, 100): explicit HSL, H ∊ [0, 360], SV ∊ [0, 100] >>> color_from_str('fF7f3f') [255, 127, 63] >>> color_from_str('0xfF7f3f') [255, 127, 63] >>> color_from_str('0XfF7f3f') [255, 127, 63] >>> color_from_str('#fF7f3f') [255, 127, 63] >>> color_from_str('Rgb(255, 127, 63)') [255, 127, 63] >>> color_from_str('Hsv(20, 75, 100)') [255, 128, 64] >>> color_from_str('Hsl(20, 100, 62)') [255, 126, 61] >>> color_from_str('fF7f3f1f') Traceback (most recent call last): ... ValueError: cannot parse color: fF7f3f1f >>> color_from_str('0bff00ff') Traceback (most recent call last): ... ValueError: cannot parse color: 0bff00ff >>> color_from_str('rgb()') Traceback (most recent call last): ... ValueError: expected 3-element triple: rgb() >>> color_from_str('rgb(255)') Traceback (most recent call last): ... ValueError: expected 3-element triple: rgb(255) >>> color_from_str('rgb(300, 255, 255)') Traceback (most recent call last): ... ValueError: expected value in range [0, 255]: 300 in rgb(300, 255, 255) >>> color_from_str('hsv(360, 150, 100)') Traceback (most recent call last): ... ValueError: expected value in range [0, 100]: 150 in hsv(360, 150, 100) >>> color_from_str('hsl(360, 100, 150)') Traceback (most recent call last): ... ValueError: expected value in range [0, 100]: 150 in hsl(360, 100, 150) """ def parse_triple(sub, maxvalues): literal = literal_eval(sub) if not isinstance(literal, tuple) or len(literal) != 3: raise ValueError(f'expected 3-element triple: {x}') for value, maxvalue in zip(literal, maxvalues): if not isinstance(value, int) and not isinstance(value, float): raise ValueError(f'expected float or int: {value} in {x}') if value < 0 or value > maxvalue: raise ValueError(f'expected value in range [0, {maxvalue}]: {value} in {x}') return literal xl = x.lower() if xl.startswith('rgb('): r, g, b = parse_triple(x[3:], (255, 255, 255)) return [r, g, b] elif xl.startswith('hsv('): h, s, v = parse_triple(x[3:], (360, 100, 100)) return list(map(lambda b: round(b*255), colorsys.hsv_to_rgb(h/360, s/100, v/100))) elif xl.startswith('hsl('): h, s, l = parse_triple(x[3:], (360, 100, 100)) return list(map(lambda b: round(b*255), colorsys.hls_to_rgb(h/360, l/100, s/100))) elif len(x) == 6: return list(bytes.fromhex(x)) elif len(x) == 7 and x.startswith('#'): return list(bytes.fromhex(x[1:])) elif len(x) == 8 and xl.startswith('0x'): return list(bytes.fromhex(x[2:])) else: raise ValueError(f'cannot parse color: {x}') def check_unsafe(*reqs, unsafe=None, error=False, **kwargs): """Check if unsafe feature requirements are met. Unstable. Checks if the requirements in the positional arguments (`*reqs`) are all met by the `unsafe` string list of enabled features. >>> check_unsafe('foo', unsafe='foo,bar') True >>> check_unsafe('foo', 'bar', unsafe='foo,bar') True >>> check_unsafe('foo', unsafe=None) False >>> check_unsafe('foo', 'baz', unsafe='foo,bar') False If `error=True` and some requirements have not been met, raises `liquidctl.error.UnsafeFeaturesNotEnabled`. In the default `error=False` mode, a boolean is return indicating whether all requirements were met. >>> check_unsafe('foo', 'baz', unsafe='foo,bar', error=True) Traceback (most recent call last): ... liquidctl.error.UnsafeFeaturesNotEnabled: baz In driver code, `unsafe` is normally passed in `**kwargs`. >>> kwargs = {'unsafe': 'foo,bar'} >>> check_unsafe('foo', 'bar', **kwargs) True >>> check_unsafe('foo', 'baz', error=True, **kwargs) Traceback (most recent call last): ... liquidctl.error.UnsafeFeaturesNotEnabled: baz """ if unsafe: reqs = tuple(filter(lambda x: x not in unsafe, reqs)) if not reqs: return True if error: raise UnsafeFeaturesNotEnabled(*reqs) return False liquidctl-1.5.1/liquidctl/version.py000066400000000000000000000000261401367561700175510ustar00rootroot00000000000000__version__ = '1.5.1' liquidctl-1.5.1/pytest.ini000066400000000000000000000000511401367561700155470ustar00rootroot00000000000000[pytest] addopts = --doctest-modules -ra liquidctl-1.5.1/setup.py000066400000000000000000000077161401367561700152470ustar00rootroot00000000000000import os import subprocess import sys import setuptools from setuptools.command.develop import develop def get_static_version(): """Read manually attributed version number. Note: the version number only changes when releases are made.""" with open('liquidctl/version.py', 'r') as fv: vals = {} exec(fv.read(), vals) return vals['__version__'] def make_pypi_long_description(doc_url): """Generate custom long description for PyPI.""" with open('README.md', 'r', encoding='utf-8') as fh: continuation = ('For which devices are supported, installation instructions, ' 'a guide to the CLI and device specific details, check the ' 'complete [Documentation]({}).').format(doc_url) long_description = (fh.read().split('', 1)[0] + continuation) return long_description def get_git_version(): """Check that `git` is accessible and return its version.""" try: return subprocess.check_output(['git', '--version']).strip().decode() except: return None def make_extraversion(editable=False): """Compile extra version information for use at runtime. Additional information will include: - values of DIST_NAME and DIST_PACKAGE environment variables - whether the installation is running in develop/editable mode - git HEAD commit hash and whether the tree is dirty """ extra = {} extra['dist_name'] = os.getenv('DIST_NAME') extra['dist_package'] = os.getenv('DIST_PACKAGE') extra['editable'] = editable if get_git_version() and os.path.isdir('.git'): rev_parse = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip().decode() describe = subprocess.check_output(['git', 'describe', '--always', '--dirty']).strip().decode() extra['commit'] = rev_parse extra['dirty'] = describe.endswith('-dirty') with open('liquidctl/extraversion.py', 'w') as fv: fv.write('__extraversion__ = {!r}'.format(extra)) class custom_develop(develop): def run(self): make_extraversion(editable=True) super().run() HOME = 'https://github.com/liquidctl/liquidctl' VERSION = get_static_version() SUPPORTED_URL = '{}/tree/v{}#supported-devices'.format(HOME, VERSION) DOC_URL = '{}/tree/v{}#liquidctl--liquid-cooler-control'.format(HOME, VERSION) CHANGES_URL = '{}/blob/v{}/CHANGELOG.md'.format(HOME, VERSION) make_extraversion() install_requires = ['docopt', 'pyusb', 'hidapi'] if sys.platform == 'linux': install_requires.append('smbus') setuptools.setup( name='liquidctl', cmdclass={'develop': custom_develop}, version=VERSION, author='Jonas Malaco', author_email='jonas@protocubo.io', description='Cross-platform tool and drivers for liquid coolers and other devices', long_description=make_pypi_long_description(DOC_URL), long_description_content_type='text/markdown', url=HOME, packages=setuptools.find_packages(), classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: End Users/Desktop', 'Intended Audience :: Developers', 'Topic :: System :: Hardware :: Hardware Drivers', 'Operating System :: OS Independent', 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', ], keywords='cross-platform cli driver corsair evga nzxt liquid-cooler fan-controller ' 'power-supply led-controller kraken smart-device hue2 gigabyte', project_urls={ 'Supported devices': SUPPORTED_URL, 'Documentation': DOC_URL, 'Changelog': CHANGES_URL, }, install_requires=install_requires, python_requires='>=3.6', entry_points={ 'console_scripts': [ 'liquidctl=liquidctl.cli:main', ], }, ) liquidctl-1.5.1/tests/000077500000000000000000000000001401367561700146645ustar00rootroot00000000000000liquidctl-1.5.1/tests/_testutils.py000066400000000000000000000115421401367561700174400ustar00rootroot00000000000000import os from collections import deque, namedtuple Report = namedtuple('Report', ['number', 'data']) def noop(*args, **kwargs): return None class MockRuntimeStorage: def __init__(self, key_prefixes): self._cache = {} def load(self, key, of_type=None, default=None): """Unstable API.""" if key in self._cache: value = self._cache[key] else: value = None if value is None: return default elif of_type and not isinstance(value, of_type): return default else: return value def store(self, key, value): """Unstable API.""" self._cache[key] = value return value class MockHidapiDevice: def __init__(self, vendor_id=None, product_id=None, release_number=None, serial_number=None, bus=None, address=None): self.vendor_id = vendor_id self.product_id = product_id self.release_number = release_number self.serial_number = serial_number self.bus = bus self.address = address self.port = None self.open = noop self.close = noop self.clear_enqueued_reports = noop self._read = deque() self.sent = list() def preload_read(self, report): self._read.append(report) def read(self, length): if self._read: number, data = self._read.popleft() if number: return [number] + list(data)[:length] else: return list(data)[:length] return None def write(self, data): data = bytes(data) # ensure data is convertible to bytes self.sent.append(Report(data[0], list(data[1:]))) return len(data) def get_feature_report(self, report_id, length): if self._read: try: report = next(filter(lambda x: x.number == report_id, self._read)) number, data = report self._read.remove(report) except StopIteration: return None # length dictates the size of the buffer, and if it's not large # enough "ioctl (GFEATURE): Value too large for defined data type" # may happen on Linux; see: # https://github.com/liquidctl/liquidctl/issues/151#issuecomment-665119675 assert length >= len(data) + 1, 'buffer not large enough for received report' return [number] + list(data)[:length] return None def send_feature_report(self, data): return self.write(data) VirtualEeprom = namedtuple('VirtualEeprom', ['name', 'data']) class VirtualSmbus: def __init__(self, address_count=256, register_count=256, name='i2c-99', description='Virtual', parent_vendor=0xff01, parent_device=0xff02, parent_subsystem_vendor=0xff10, parent_subsystem_device=0xff20, parent_driver='virtual'): self._open = False self._data = [[0] * register_count for _ in range(address_count)] self.name = name self.description = description self.parent_vendor = parent_vendor self.parent_device = parent_device self.parent_subsystem_vendor = parent_subsystem_vendor self.parent_subsystem_device = parent_subsystem_device self.parent_driver = parent_driver def open(self): self._open = True def read_byte(self, address): if not self._open: raise OSError('closed') return self._data[address][0] def read_byte_data(self, address, register): if not self._open: raise OSError('closed') return self._data[address][register] def read_word_data(self, address, register): if not self._open: raise OSError('closed') return self._data[address][register] def read_block_data(self, address, register): if not self._open: raise OSError('closed') return self._data[address][register] def write_byte(self, address, value): if not self._open: raise OSError('closed') self._data[address][0] = value def write_byte_data(self, address, register, value): if not self._open: raise OSError('closed') self._data[address][register] = value def write_word_data(self, address, register, value): if not self._open: raise OSError('closed') self._data[address][register] = value def write_block_data(self, address, register, data): if not self._open: raise OSError('closed') self._data[address][register] = data def close(self): self._open = False def emulate_eeprom_at(self, address, name, data): self._data[address] = VirtualEeprom(name, data) # hack def load_eeprom(self, address): return self._data[address] # hack liquidctl-1.5.1/tests/test_api.py000066400000000000000000000017511401367561700170520ustar00rootroot00000000000000import pytest from liquidctl.driver.base import BaseDriver class Virtual(BaseDriver): def __init__(self, **kwargs): self.kwargs = dict() self.kwargs['__init__'] = kwargs self.connected = False def connect(self, **kwargs): self.kwargs['connect'] = kwargs self.connected = True return self def disconnect(self, **kwargs): self.kwargs['disconnect'] = kwargs self.connected = False def test_connects_and_disconnects_with_context_manager(): dev = Virtual() with pytest.raises(RuntimeError): with dev.connect(): assert dev.connected raise RuntimeError() assert not dev.connected def test_entering_the_runtime_context_does_not_call_connect(): dev = Virtual() with dev.connect(marker=True): # since __enter__ takes no arguments, if __enter__ calls connect it # will override dev.kwargs['connect'] with {} assert 'marker' in dev.kwargs['connect'] liquidctl-1.5.1/tests/test_asetek.py000066400000000000000000000104021401367561700175460ustar00rootroot00000000000000import pytest from collections import deque from liquidctl.driver.asetek import Modern690Lc, Legacy690Lc, Hydro690Lc from _testutils import noop class _Mock690LcDevice(): def __init__(self, vendor_id=None, product_id=None, release_number=None, serial_number=None, bus=None, address=None, port=None): self.vendor_id = vendor_id self.product_id = product_id self.release_numer = release_number self.serial_number = serial_number self.bus = bus self.address = address self.port = port self.open = noop self.claim = noop self.release = noop self.close = noop self._reset_sent() def read(self, endpoint, length, timeout=None): return [0] * length def write(self, endpoint, data, timeout=None): self._sent_xfers.append(('write', endpoint, data)) def ctrl_transfer(self, bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength=None, timeout=None): self._sent_xfers.append(('ctrl_transfer', bmRequestType, bRequest, wValue, wIndex, data_or_wLength)) def _reset_sent(self): self._sent_xfers = deque() @pytest.fixture def mockModern690LcDevice(): device = _Mock690LcDevice() dev = Modern690Lc(device, 'Mock Modern 690LC') dev.connect() return dev @pytest.fixture def mockLegacy690LcDevice(): device = _Mock690LcDevice(vendor_id=0xffff, product_id=0xb200, bus=1, port=(1,)) dev = Legacy690Lc(device, 'Mock Legacy 690LC') dev.connect() return dev def test_modern690Lc_device_not_totally_broken(mockModern690LcDevice): """A few reasonable example calls do not raise exceptions.""" dev = mockModern690LcDevice dev.initialize() dev.get_status() dev.set_color(channel='led', mode='blinking', colors=iter([[3, 2, 1]]), time_per_color=3, time_off=1, alert_threshold=42, alert_color=[90, 80, 10]) dev.set_color(channel='led', mode='rainbow', colors=[], speed=5) dev.set_speed_profile(channel='fan', profile=iter([(20, 20), (30, 50), (40, 100)])) dev.set_fixed_speed(channel='pump', duty=50) def test_modern690Lc_device_connect(mockModern690LcDevice): def mock_open(): nonlocal opened opened = True mockModern690LcDevice.device.open = mock_open opened = False with mockModern690LcDevice.connect() as cm: assert cm == mockModern690LcDevice assert opened def test_modern690Lc_device_begin_transaction(mockModern690LcDevice): mockModern690LcDevice.device._reset_sent() mockModern690LcDevice.get_status() (begin, _) = mockModern690LcDevice.device._sent_xfers xfer_type, bmRequestType, bRequest, wValue, wIndex, datalen = begin assert xfer_type == 'ctrl_transfer' assert bRequest == 2 assert bmRequestType == 0x40 assert wValue == 1 assert wIndex == 0 assert datalen is None def test_legacy690Lc_device_not_totally_broken(mockLegacy690LcDevice): """A few reasonable example calls do not raise exceptions.""" dev = mockLegacy690LcDevice dev.initialize() status = dev.get_status() dev.set_color(channel='led', mode='blinking', colors=iter([[3, 2, 1]]), time_per_color=3, time_off=1, alert_threshold=42, alert_color=[90, 80, 10]) dev.set_fixed_speed(channel='fan', duty=80) dev.set_fixed_speed(channel='pump', duty=50) def test_legacy690Lc_device_matches_leviathan_updates(mockLegacy690LcDevice): dev = mockLegacy690LcDevice dev.initialize() dev.set_fixed_speed(channel='pump', duty=50) dev.device._reset_sent() dev.set_color(channel='led', mode='fading', colors=[[0, 0, 255], [0, 255, 0]], time_per_color=1, alert_threshold=60, alert_color=[0, 0, 0]) _begin, (color_msgtype, color_ep, color_data) = dev.device._sent_xfers assert color_msgtype == 'write' assert color_ep == 2 assert color_data[0:12] == [0x10, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0x3c, 1] dev.device._reset_sent() dev.set_fixed_speed(channel='fan', duty=50) _begin, pump_message, fan_message = dev.device._sent_xfers assert pump_message == ('write', 2, [0x13, 50]) assert fan_message == ('write', 2, [0x12, 50]) liquidctl-1.5.1/tests/test_backwards_compatibility_10.py000066400000000000000000000027771401367561700235040ustar00rootroot00000000000000"""Test the use of APIs from liquidctl v1.0.0. While at the time all APIs were undocumented, we choose to support the use cases from GKraken as that software is a substantial contribution to the community. """ import pytest from liquidctl.driver.kraken_two import KrakenTwoDriver from liquidctl.version import __version__ from _testutils import MockHidapiDevice SPECTRUM = [ (235, 77, 40), (255, 148, 117), (126, 66, 45), (165, 87, 0), (56, 193, 66), (116, 217, 170), (166, 158, 255), (208, 0, 122) ] @pytest.fixture def mockDevice(): device = MockHidapiDevice() dev = KrakenTwoDriver(device, 'Mock X62', device_type=KrakenTwoDriver.DEVICE_KRAKENX) dev.connect() return dev def test_pre11_apis_find_does_not_raise(): import liquidctl.cli liquidctl.cli.find_all_supported_devices() def test_pre11_apis_connect_as_initialize(mockDevice): # deprecated behavior in favor of connect() mockDevice.initialize() def test_pre11_apis_deprecated_super_mode(mockDevice): # deprecated in favor of super-fixed, super-breathing and super-wave mockDevice.set_color('sync', 'super', [(128, 0, 255)] + SPECTRUM, 'normal') def test_pre11_apis_status_order(mockDevice): # GKraken unreasonably expects a particular ordering pass def test_pre11_apis_finalize_as_connect_or_noop(mockDevice): # deprecated in favor of disconnect() mockDevice.finalize() # should disconnect mockDevice.finalize() # should be a no-op liquidctl-1.5.1/tests/test_backwards_compatibility_11.py000066400000000000000000000030711401367561700234710ustar00rootroot00000000000000"""Test backwards compatibility with liquidctl 1.1.0.""" from liquidctl.driver.kraken2 import Kraken2 from liquidctl.driver.usb import hid, HidapiDevice import usb import pytest class _MockPyUsbHandle(usb.core.Device): def __init__(self, serial_number): self.idVendor = 0x1e71 self.idProduct = 0x170e self._serial_number = serial_number class MockResourceManager(): def dispose(self, *args, **kwargs): pass self._ctx = MockResourceManager() def _mock_enumerate(vendor_id=0, product_id=0): return [ {'vendor_id': vendor_id, 'product_id': product_id, 'serial_number': '987654321'}, {'vendor_id': vendor_id, 'product_id': product_id, 'serial_number': '123456789'} ] def test_construct_with_raw_pyusb_handle(monkeypatch): monkeypatch.setattr(hid, 'enumerate', _mock_enumerate) pyusb_handle = _MockPyUsbHandle(serial_number='123456789') liquidctl_device = Kraken2(pyusb_handle, 'Some device') assert liquidctl_device.device.vendor_id == pyusb_handle.idVendor, \ '.device points to incorrect physical device' assert liquidctl_device.device.product_id == pyusb_handle.idProduct, \ '.device points to incorrect physical device' assert liquidctl_device.device.serial_number == pyusb_handle.serial_number, \ '.device points to different physical unit' assert isinstance(liquidctl_device.device, HidapiDevice), \ '.device not properly converted to HidapiDevice instance' liquidctl-1.5.1/tests/test_backwards_compatibility_12.py000066400000000000000000000001741401367561700234730ustar00rootroot00000000000000import pytest def test_pre13_old_driver_names(): from liquidctl.driver.nzxt_smart_device import NzxtSmartDeviceDriver liquidctl-1.5.1/tests/test_backwards_compatibility_13.py000066400000000000000000000014241401367561700234730ustar00rootroot00000000000000import pytest def test_pre14_old_module_names(): import liquidctl.driver.asetek import liquidctl.driver.corsair_hid_psu import liquidctl.driver.kraken_two import liquidctl.driver.nzxt_smart_device import liquidctl.driver.seasonic def test_pre14_old_driver_names(): from liquidctl.driver.asetek import AsetekDriver from liquidctl.driver.asetek import LegacyAsetekDriver from liquidctl.driver.asetek import CorsairAsetekDriver from liquidctl.driver.corsair_hid_psu import CorsairHidPsuDriver from liquidctl.driver.kraken_two import KrakenTwoDriver from liquidctl.driver.nzxt_smart_device import SmartDeviceDriver from liquidctl.driver.nzxt_smart_device import SmartDeviceV2Driver from liquidctl.driver.seasonic import SeasonicEDriver liquidctl-1.5.1/tests/test_backwards_compatibility_14.py000066400000000000000000000116401401367561700234750ustar00rootroot00000000000000import pytest from _testutils import MockHidapiDevice RADICAL_RED = [0xff, 0x35, 0x5e] MOUNTAIN_MEADOW = [0x1a, 0xb3, 0x85] def test_find_from_driver_package_still_available(): from liquidctl.driver import find_liquidctl_devices def test_kraken2_backwards_modes_are_deprecated(caplog): modes = ['backwards-spectrum-wave', 'backwards-marquee-3', 'backwards-marquee-4', 'backwards-marquee-5', 'backwards-marquee-6', 'covering-backwards-marquee', 'backwards-moving-alternating', 'backwards-super-wave'] from liquidctl.driver.kraken2 import Kraken2 for mode in modes: base_mode = mode.replace('backwards-', '') old = Kraken2(MockHidapiDevice(), 'Mock X62', device_type=Kraken2.DEVICE_KRAKENX) new = Kraken2(MockHidapiDevice(), 'Mock X62', device_type=Kraken2.DEVICE_KRAKENX) colors = [RADICAL_RED, MOUNTAIN_MEADOW] old.set_color('ring', mode, colors) new.set_color('ring', base_mode, colors, direction='backward') assert old.device.sent == new.device.sent, \ f'{mode} != {base_mode} + direction=backward' assert 'deprecated mode' in caplog.text def test_kraken3_backwards_modes_are_deprecated(caplog): modes = ['backwards-spectrum-wave', 'backwards-marquee-3', 'backwards-marquee-4', 'backwards-marquee-5', 'backwards-marquee-6', 'backwards-moving-alternating-3', 'covering-backwards-marquee', 'backwards-moving-alternating-4', 'backwards-moving-alternating-5', 'backwards-moving-alternating-6', 'backwards-rainbow-flow', 'backwards-super-rainbow', 'backwards-rainbow-pulse'] from liquidctl.driver.kraken3 import KrakenX3 from liquidctl.driver.kraken3 import _COLOR_CHANNELS_KRAKENX from liquidctl.driver.kraken3 import _SPEED_CHANNELS_KRAKENX for mode in modes: base_mode = mode.replace('backwards-', '') old = KrakenX3(MockHidapiDevice(), 'Mock X63', speed_channels=_SPEED_CHANNELS_KRAKENX, color_channels=_COLOR_CHANNELS_KRAKENX) new = KrakenX3(MockHidapiDevice(), 'Mock X63', speed_channels=_SPEED_CHANNELS_KRAKENX, color_channels=_COLOR_CHANNELS_KRAKENX) colors = [RADICAL_RED, MOUNTAIN_MEADOW] old.set_color('ring', mode, colors) new.set_color('ring', base_mode, colors, direction='backward') assert old.device.sent == new.device.sent, \ f'{mode} != {base_mode} + direction=backward' assert 'deprecated mode' in caplog.text def test_smart_device_v1_backwards_modes_are_deprecated(caplog): modes = ['backwards-spectrum-wave', 'backwards-marquee-3', 'backwards-marquee-4', 'backwards-marquee-5', 'backwards-marquee-6', 'covering-backwards-marquee', 'backwards-moving-alternating', 'backwards-super-wave'] from liquidctl.driver.smart_device import SmartDevice for mode in modes: base_mode = mode.replace('backwards-', '') old = SmartDevice(MockHidapiDevice(), 'Mock Smart Device', speed_channel_count=3, color_channel_count=1) new = SmartDevice(MockHidapiDevice(), 'Mock Smart Device', speed_channel_count=3, color_channel_count=1) colors = [RADICAL_RED, MOUNTAIN_MEADOW] old.set_color('led', mode, colors) new.set_color('led', base_mode, colors, direction='backward') assert old.device.sent == new.device.sent, \ f'{mode} != {base_mode} + direction=backward' assert 'deprecated mode' in caplog.text def test_hue2_backwards_modes_are_deprecated(caplog): modes = ['backwards-spectrum-wave', 'backwards-marquee-3', 'backwards-marquee-4', 'backwards-marquee-5', 'backwards-marquee-6', 'backwards-moving-alternating-3', 'covering-backwards-marquee', 'backwards-moving-alternating-4', 'backwards-moving-alternating-5', 'backwards-moving-alternating-6', 'backwards-rainbow-flow', 'backwards-super-rainbow', 'backwards-rainbow-pulse'] from liquidctl.driver.smart_device import SmartDevice2 for mode in modes: base_mode = mode.replace('backwards-', '') old = SmartDevice2(MockHidapiDevice(), 'Mock Smart Device V2', speed_channel_count=3, color_channel_count=2) new = SmartDevice2(MockHidapiDevice(), 'Mock Smart Device V2', speed_channel_count=3, color_channel_count=2) colors = [RADICAL_RED, MOUNTAIN_MEADOW] old.set_color('led1', mode, colors) new.set_color('led1', base_mode, colors, direction='backward') assert old.device.sent == new.device.sent, \ f'{mode} != {base_mode} + direction=backward' assert 'deprecated mode' in caplog.text liquidctl-1.5.1/tests/test_comander_pro.py000066400000000000000000001277331401367561700207620ustar00rootroot00000000000000import pytest from liquidctl.driver.commander_pro import _quoted, _prepare_profile, _get_fan_mode_description, CommanderPro from liquidctl.error import NotSupportedByDevice from _testutils import MockHidapiDevice, Report, MockRuntimeStorage # hardcoded responce data expected for some of the calls: # commander pro: firmware request (0.9.214) # commander pro: bootloader req (2.3) # commander pro: get temp config ( 3 sensors) # commander pro: get fan configs (3 DC fans, 1 PWM fan) # note I have not tested it with pwm fans # commander pro: @pytest.fixture def commanderProDeviceUnconnected(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c10, address='addr') return CommanderPro(device, 'Corsair Commander Pro (experimental)', 6, 4, 2) @pytest.fixture def lightingNodeProDeviceUnconnected(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c0b, address='addr') return CommanderPro(device, 'Corsair Lighting Node Pro (experimental)', 0, 0, 2) @pytest.fixture def commanderProDevice(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c10, address='addr') pro = CommanderPro(device, 'Corsair Commander Pro (experimental)', 6, 4, 2) pro.connect() pro._data = MockRuntimeStorage(key_prefixes='testing') return pro @pytest.fixture def lightingNodeProDevice(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c0b, address='addr') node = CommanderPro(device, 'Corsair Lighting Node Pro (experimental)', 0, 0, 2) node.connect() node._data = MockRuntimeStorage(key_prefixes='testing') return node # prepare profile def test_prepare_profile_valid_max_rpm(): assert _prepare_profile([[10, 400], [20, 5000]], 60) == [(10, 400), (20, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_add_max_rpm(): assert _prepare_profile([[10, 400]], 60) == [(10, 400), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] assert _prepare_profile([[10, 400], [20, 500], [30, 600], [40, 700], [50, 800]], 60) == [(10, 400), (20, 500), (30, 600), (40, 700), (50, 800), (60, 5000)] def test_prepare_profile_missing_max_rpm(): with pytest.raises(ValueError): _prepare_profile([[10, 400], [20, 500], [30, 600], [40, 700], [50, 800], [55, 900]], 60) def test_prepare_profile_full_set(): assert _prepare_profile([[10, 400], [20, 500], [30, 600], [40, 700], [45, 2000], [50, 5000]], 60) == [(10, 400), (20, 500), (30, 600), (40, 700), (45, 2000), (50, 5000)] def test_prepare_profile_too_many_points(): with pytest.raises(ValueError): _prepare_profile([[10, 400], [20, 500], [30, 600], [40, 700], [50, 800], [55, 900]], 60) def test_prepare_profile_no_points(): assert _prepare_profile([], 60) == [(60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_empty_list(): assert _prepare_profile([], 60) == [(60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_above_max_temp(): assert _prepare_profile([[10, 400], [70, 2000]], 60) == [(10, 400), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_temp_low(): assert _prepare_profile([[-10, 400], [70, 2000]], 60) == [(-10, 400), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_max_temp(): assert _prepare_profile([], 100) == [(100, 5000), (100, 5000), (100, 5000), (100, 5000), (100, 5000), (100, 5000)] # quoted def test_quoted_empty(): assert _quoted() == '' def test_quoted_single(): assert _quoted('one arg') == "'one arg'" def test_quoted_valid(): assert _quoted('one', 'two') == "'one', 'two'" def test_quoted_not_string(): assert _quoted('test', 500) == "'test', 500" # fan modes def test_get_fan_mode_description_auto(): assert _get_fan_mode_description(0x00) == 'Auto/Disconnected' def test_get_fan_mode_description_unknown(): assert _get_fan_mode_description(0x03) == 'UNKNOWN' assert _get_fan_mode_description(0x04) == 'UNKNOWN' assert _get_fan_mode_description(0x10) == 'UNKNOWN' assert _get_fan_mode_description(0xff) == 'UNKNOWN' def test_get_fan_mode_description_dc(): assert _get_fan_mode_description(0x01) == 'DC' def test_get_fan_mode_description_pwm(): assert _get_fan_mode_description(0x02) == 'PWM' # class methods def test_commander_constructor(commanderProDeviceUnconnected): assert commanderProDeviceUnconnected._data is None assert commanderProDeviceUnconnected._fan_names == ['fan1', 'fan2', 'fan3', 'fan4', 'fan5', 'fan6'] assert commanderProDeviceUnconnected._led_names == ['led1', 'led2'] assert commanderProDeviceUnconnected._temp_probs == 4 assert commanderProDeviceUnconnected._fan_count == 6 def test_lighting_constructor(lightingNodeProDeviceUnconnected): assert lightingNodeProDeviceUnconnected._data is None assert lightingNodeProDeviceUnconnected._fan_names == [] assert lightingNodeProDeviceUnconnected._led_names == ['led1', 'led2'] assert lightingNodeProDeviceUnconnected._temp_probs == 0 assert lightingNodeProDeviceUnconnected._fan_count == 0 def test_connect_commander(commanderProDeviceUnconnected): commanderProDeviceUnconnected.connect() assert commanderProDeviceUnconnected._data is not None def test_connect_lighting(lightingNodeProDeviceUnconnected): lightingNodeProDeviceUnconnected.connect() assert lightingNodeProDeviceUnconnected._data is not None def test_initialize_commander_pro(commanderProDevice): responses = [ '000009d4000000000000000000000000', # firmware '00000500000000000000000000000000', # bootloader '00010100010000000000000000000000', # temp probes '00010102000000000000000000000000' # fan probes ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) res = commanderProDevice.initialize() assert len(res) == 12 assert res[0][1] == '0.9.212' assert res[1][1] == '0.5' assert res[2][1] == 'Connected' assert res[3][1] == 'Connected' assert res[4][1] == 'Not Connected' assert res[5][1] == 'Connected' assert res[6][1] == 'DC' assert res[7][1] == 'DC' assert res[8][1] == 'PWM' assert res[9][1] == 'Auto/Disconnected' assert res[10][1] == 'Auto/Disconnected' assert res[11][1] == 'Auto/Disconnected' data = commanderProDevice._data.load('fan_modes', None) assert data is not None assert len(data) == 6 assert data[0] == 0x01 assert data[1] == 0x01 assert data[2] == 0x02 assert data[3] == 0x00 assert data[4] == 0x00 assert data[5] == 0x00 data = commanderProDevice._data.load('temp_sensors_connected', None) assert data is not None assert len(data) == 4 assert data[0] == 0x01 assert data[1] == 0x01 assert data[2] == 0x00 assert data[3] == 0x01 def test_initialize_lighting_node(lightingNodeProDevice): responses = [ '000009d4000000000000000000000000', # firmware '00000500000000000000000000000000' # bootloader ] for d in responses: lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(d))) res = lightingNodeProDevice.initialize() assert len(res) == 2 assert res[0][1] == '0.9.212' assert res[1][1] == '0.5' data = lightingNodeProDevice._data.load('fan_modes', None) assert data is None data = lightingNodeProDevice._data.load('temp_sensors_connected', None) assert data is None def test_get_status_commander_pro(commanderProDevice): responses = [ '000a8300000000000000000000000000', # temp sensor 1 '000b6a00000000000000000000000000', # temp sensor 2 '000a0e00000000000000000000000000', # temp sensor 4 '002f2200000000000000000000000000', # get 12v '00136500000000000000000000000000', # get 5v '000d1f00000000000000000000000000', # get 3.3v '0003ac00000000000000000000000000', # fan speed 1 '0003ab00000000000000000000000000', # fan speed 2 '0003db00000000000000000000000000' # fan speed 3 ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x02, 0x00, 0x00, 0x00]) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) res = commanderProDevice.get_status() assert len(res) == 13 # voltages assert res[0][1] == 12.066 # 12v assert res[1][1] == 4.965 # 5v assert res[2][1] == 3.359 # 3.3v # temp probes assert res[3][1] == 26.91 assert res[4][1] == 29.22 assert res[5][1] == 0.0 assert res[6][1] == 25.74 # fans rpm assert res[7][1] == 940 assert res[8][1] == 939 assert res[9][1] == 987 assert res[10][1] == 0 assert res[11][1] == 0 assert res[12][1] == 0 # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 9 assert sent[0].data[0] == 0x11 assert sent[1].data[0] == 0x11 assert sent[2].data[0] == 0x11 assert sent[3].data[0] == 0x12 assert sent[4].data[0] == 0x12 assert sent[5].data[0] == 0x12 assert sent[6].data[0] == 0x21 assert sent[7].data[0] == 0x21 assert sent[8].data[0] == 0x21 def test_get_status_lighting_pro(lightingNodeProDevice): res = lightingNodeProDevice.get_status() assert len(res) == 0 def test_get_temp_valid_sensor_commander(commanderProDevice): response = '000a8300000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x01, 0x01]) res = commanderProDevice._get_temp(1) assert res == 26.91 # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x11 assert sent[0].data[1] == 1 def test_get_temp_invalid_sensor_low_commander(commanderProDevice): response = '000a8300000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x01, 0x01]) with pytest.raises(ValueError): commanderProDevice._get_temp(-1) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_get_temp_invalid_sensor_high_commander(commanderProDevice): response = '000a8300000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x01, 0x01]) with pytest.raises(ValueError): commanderProDevice._get_temp(4) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_get_temp_lighting(lightingNodeProDevice): response = '000a8300000000000000000000000000' lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(response))) lightingNodeProDevice._data.store('temp_sensors_connected', [0x00, 0x00, 0x00, 0x00]) with pytest.raises(ValueError): lightingNodeProDevice._get_temp(2) # check the commands sent sent = lightingNodeProDevice.device.sent assert len(sent) == 0 def test_get_fan_rpm_valid_commander(commanderProDevice): response = '0003ac00000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x02, 0x00, 0x00, 0x00]) res = commanderProDevice._get_fan_rpm(1) assert res == 940 # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x21 assert sent[0].data[1] == 1 def test_get_fan_rpm_invalid_low_commander(commanderProDevice): response = '0003ac00000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x02, 0x00, 0x00, 0x00]) with pytest.raises(ValueError): commanderProDevice._get_fan_rpm(-1) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_get_fan_rpm_invalid_high_commander(commanderProDevice): response = '0003ac00000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x02, 0x00, 0x00, 0x00]) with pytest.raises(ValueError): commanderProDevice._get_fan_rpm(7) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_get_fan_rpm_lighting(lightingNodeProDevice): response = '0003ac00000000000000000000000000' lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(response))) with pytest.raises(ValueError): lightingNodeProDevice._get_fan_rpm(7) # check the commands sent sent = lightingNodeProDevice.device.sent assert len(sent) == 0 def test_get_hw_fan_channels_all(commanderProDevice): res = commanderProDevice._get_hw_fan_channels('sync') assert res == [0, 1, 2, 3, 4, 5] def test_get_hw_fan_channels_uppercase(commanderProDevice): res = commanderProDevice._get_hw_fan_channels('FaN3') assert res == [2] def test_get_hw_fan_channels_lowercase(commanderProDevice): res = commanderProDevice._get_hw_fan_channels('fan2') assert res == [1] def test_get_hw_fan_channels_invalid(commanderProDevice): with pytest.raises(ValueError): commanderProDevice._get_hw_fan_channels('fan23') with pytest.raises(ValueError): commanderProDevice._get_hw_fan_channels('fan7') with pytest.raises(ValueError): commanderProDevice._get_hw_fan_channels('fan0') with pytest.raises(ValueError): commanderProDevice._get_hw_fan_channels('bob') def test_get_hw_led_channels_all(commanderProDevice): res = commanderProDevice._get_hw_led_channels('led') assert res == [0, 1] def test_get_hw_led_channels_uppercase(commanderProDevice): res = commanderProDevice._get_hw_led_channels('LeD2') assert res == [1] def test_get_hw_led_channels_lowercase(commanderProDevice): res = commanderProDevice._get_hw_led_channels('led1') assert res == [0] def test_get_hw_led_channels_invalid(commanderProDevice): with pytest.raises(ValueError): commanderProDevice._get_hw_led_channels('led0') with pytest.raises(ValueError): commanderProDevice._get_hw_led_channels('led3') with pytest.raises(ValueError): commanderProDevice._get_hw_led_channels('bob') def test_set_fixed_speed_low(commanderProDevice): response = '00000000000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x01, 0x01, 0x01, 0x01]) commanderProDevice.set_fixed_speed('fan4', -10) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x23 assert sent[0].data[1] == 0x03 assert sent[0].data[2] == 0x00 def test_set_fixed_speed_high(commanderProDevice): response = '00000000000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x01, 0x01, 0x01, 0x01]) commanderProDevice.set_fixed_speed('fan3', 110) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x23 assert sent[0].data[1] == 0x02 assert sent[0].data[2] == 0x64 def test_set_fixed_speed_valid(commanderProDevice): response = '00000000000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x01, 0x01, 0x01, 0x01]) commanderProDevice.set_fixed_speed('fan2', 50) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x23 assert sent[0].data[1] == 0x01 assert sent[0].data[2] == 0x32 def test_set_fixed_speed_valid_unconfigured(commanderProDevice): response = '00000000000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) commanderProDevice.set_fixed_speed('fan2', 50) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_set_fixed_speed_valid_multi_fan(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) commanderProDevice.set_fixed_speed('sync', 50) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 3 assert sent[0].data[0] == 0x23 assert sent[0].data[1] == 0x00 assert sent[0].data[2] == 0x32 assert sent[1].data[0] == 0x23 assert sent[1].data[1] == 0x02 assert sent[1].data[2] == 0x32 assert sent[2].data[0] == 0x23 assert sent[2].data[1] == 0x03 assert sent[2].data[2] == 0x32 def test_set_fixed_speed_lighting(lightingNodeProDevice): response = '00000000000000000000000000000000' lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(response))) with pytest.raises(NotSupportedByDevice): lightingNodeProDevice.set_fixed_speed('sync', 50) # check the commands sent sent = lightingNodeProDevice.device.sent assert len(sent) == 0 def test_set_speed_profile_valid_multi_fan(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) commanderProDevice.set_speed_profile('sync', [(10, 500), (20, 1000)]) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 3 assert sent[0].data[0] == 0x25 assert sent[0].data[1] == 0x00 assert sent[0].data[2] == 0x00 assert sent[0].data[3] == 0x03 assert sent[0].data[4] == 0xe8 assert sent[0].data[15] == 0x01 assert sent[0].data[16] == 0xf4 assert sent[1].data[0] == 0x25 assert sent[1].data[1] == 0x02 assert sent[1].data[2] == 0x00 assert sent[2].data[0] == 0x25 assert sent[2].data[1] == 0x03 assert sent[2].data[2] == 0x00 def test_set_speed_profile_invalid_temp_sensor(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) commanderProDevice.set_speed_profile('fan1', [(10, 500), (20, 1000)], temperature_sensor=10) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x25 assert sent[0].data[1] == 0x00 assert sent[0].data[2] == 0x03 assert sent[0].data[3] == 0x03 assert sent[0].data[4] == 0xe8 assert sent[0].data[15] == 0x01 assert sent[0].data[16] == 0xf4 def test_set_speed_profile_no_temp_sensors(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('temp_sensors_connected', [0x00, 0x00, 0x00, 0x00]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) with pytest.raises(ValueError): commanderProDevice.set_speed_profile('sync', [(10, 500), (20, 1000)], temperature_sensor=1) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_set_speed_profile_valid(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) commanderProDevice.set_speed_profile('fan3', [(10, 500), (20, 1000)]) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x25 assert sent[0].data[1] == 0x02 assert sent[0].data[2] == 0x00 assert sent[0].data[3] == 0x03 assert sent[0].data[4] == 0xe8 assert sent[0].data[15] == 0x01 assert sent[0].data[16] == 0xf4 def test_set_speed_profile_lighting(lightingNodeProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(d))) lightingNodeProDevice._data.store('temp_sensors_connected', [0x01, 0x00, 0x00, 0x00]) lightingNodeProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) with pytest.raises(NotSupportedByDevice): lightingNodeProDevice.set_speed_profile('sync', [(10, 500), (20, 1000)]) # check the commands sent sent = lightingNodeProDevice.device.sent assert len(sent) == 0 def test_set_speed_profile_valid_unconfigured(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('temp_sensors_connected', [0x00, 0x00, 0x00, 0x00]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) with pytest.raises(ValueError): commanderProDevice.set_speed_profile('fan2', [(10, 500), (20, 1000)]) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_set_color_hardware_clear(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [] } commanderProDevice._data.store('saved_effects', [effect]) for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice.set_color('led1', 'clear', [], ) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is None def test_set_color_hardware_off(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [] } commanderProDevice._data.store('saved_effects', [effect]) for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice.set_color('led1', 'off', []) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[4] == 0x04 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is None @pytest.mark.parametrize('directionStr,expected', [ ('forward', 0x01), ('FORWARD', 0x01), ('fOrWaRd', 0x01), ('backward', 0x00), ('BACKWARD', 0x00), ('BaCkWaRd', 0x00) ]) def test_set_color_hardware_dirrection(commanderProDevice, directionStr, expected): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, direction=directionStr) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[6] == expected effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_direction_default(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[6] == 0x01 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_speed_default(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[5] == 0x01 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('speedStr,expected', [ ('slow', 0x02), ('SLOW', 0x02), ('SlOw', 0x02), ('fast', 0x00), ('FAST', 0x00), ('fAsT', 0x00), ('medium', 0x01), ('MEDIUM', 0x01), ('MeDiUm', 0x01) ]) def test_set_color_hardware_speed(commanderProDevice, speedStr, expected): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, speed=speedStr) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[5] == expected effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_default_start_end(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[2] == 0x00 # start led assert sent[3].data[3] == 0x01 # num leds effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('startLED,expected', [ (1, 0x00), (30, 0x1d), (92, 0x5b) ]) def test_set_color_hardware_start_set(commanderProDevice, startLED, expected): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, start_led=startLED) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[2] == expected # start led effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('numLED,expected', [ (1, 0x01), (30, 0x1e), (96, 0x5f) ]) def test_set_color_hardware_num_leds(commanderProDevice, numLED, expected): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, start_led=1, maximum_leds=numLED) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[3] == expected # num leds effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_too_many_leds(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, start_led=50, maximum_leds=50) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[2] == 0x31 # start led assert sent[3].data[3] == 0x2e # num led effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_too_few_leds(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, start_led=1, maximum_leds=0) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[2] == 0x00 # start led assert sent[3].data[3] == 0x01 # num led effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('channel,expected', [ ('led1', 0x00), ('led', 0x00), ('LeD1', 0x00), ('led2', 0x01), ('LED2', 0x01), ('LeD2', 0x01) ]) def test_set_color_hardware_channel(commanderProDevice, channel, expected): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color(channel, 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[0].data[1] == expected assert sent[1].data[1] == expected assert sent[2].data[1] == expected assert sent[3].data[0] == 0x35 assert sent[3].data[1] == expected effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_random_color(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[7] == 0x01 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_not_random_color(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[7] == 0x00 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_too_many_colors(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc], [0x00, 0x11, 0x22], [0x33, 0x44, 0x55]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[7] == 0x00 assert sent[3].data[9] == 0xaa assert sent[3].data[10] == 0xbb assert sent[3].data[11] == 0xcc effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_too_few_colors(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice.set_color('led1', 'fixed', []) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[7] == 0x01 assert sent[3].data[9] == 0x00 assert sent[3].data[10] == 0x00 assert sent[3].data[11] == 0x00 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('modeStr,expected', [ ('rainbow', 0x00), ('color_shift', 0x01), ('color_pulse', 0x02), ('color_wave', 0x03), ('fixed', 0x04), ('visor', 0x06), ('marquee', 0x07), ('blink', 0x08), ('sequential', 0x09), ('sEqUeNtIaL', 0x09), ('rainbow2', 0x0a) ]) def test_set_color_hardware_valid_mode(commanderProDevice, modeStr, expected): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice.set_color('led1', modeStr, []) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[4] == expected effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_invalid_mode(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) colors = [[0xaa, 0xbb, 0xcc]] with pytest.raises(ValueError): commanderProDevice.set_color('led1', 'invalid', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is None def test_set_color_hardware_multipe_commands(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [0xaa, 0xbb, 0xcc] } commanderProDevice._data.store('saved_effects', [effect]) commanderProDevice.set_color('led1', 'fixed', [[0x00, 0x11, 0x22]], start_led=16, maximum_leds=5) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 6 assert sent[3].data[0] == 0x35 assert sent[4].data[0] == 0x35 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 2 def test_send_command_valid_data(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._send_command(6, [255, 0, 20, 10, 15]) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert len(sent[0].data) == 64 assert sent[0].data[0] == 6 assert sent[0].data[1] == 255 def test_send_command_no_data(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._send_command(6) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert len(sent[0].data) == 64 assert sent[0].data[0] == 6 assert sent[0].data[1] == 0 def test_send_command_data_too_long(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) data = bytearray(100) commanderProDevice._send_command(3, data) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert len(sent[0].data) == 64 assert sent[0].data[0] == 3 assert sent[0].data[1] == 0 liquidctl-1.5.1/tests/test_corsair_hid_psu.py000066400000000000000000000020661401367561700214560ustar00rootroot00000000000000import pytest from liquidctl.driver.corsair_hid_psu import CorsairHidPsu from _testutils import MockHidapiDevice, Report class _MockPsuDevice(MockHidapiDevice): def write(self, data): super().write(data) data = data[1:] # skip unused report ID reply = bytearray(64) if data[1] in [0xd8, 0xf0]: reply[2] = 1 # just a valid mode self.preload_read(Report(0, reply)) @pytest.fixture def mockPsuDevice(): device = _MockPsuDevice(vendor_id=0x1b1c, product_id=0x1c05, address='addr') return CorsairHidPsu(device, 'mock Corsair HX750i PSU') def test_corsair_psu_not_totally_broken(mockPsuDevice): mockPsuDevice.set_fixed_speed(channel='fan', duty=50) report_id, report_data = mockPsuDevice.device.sent[0] assert report_id == 0 assert len(report_data) == 64 def test_corsair_psu_dont_inject_report_ids(mockPsuDevice): mockPsuDevice.set_fixed_speed(channel='fan', duty=50) report_id, report_data = mockPsuDevice.device.sent[0] assert report_id == 0 assert len(report_data) == 64 liquidctl-1.5.1/tests/test_ddr4.py000066400000000000000000000307611401367561700171410ustar00rootroot00000000000000from liquidctl.driver.ddr4 import * from liquidctl.error import * import pytest from _testutils import VirtualSmbus # SPD samples def patch_spd_dump(spd_dump, slice, new): spd_dump = bytearray(spd_dump) spd_dump[slice] = new return bytes(spd_dump) _VENGEANCE_RGB_SAMPLE = bytes.fromhex( '23100c028521000800000003090300000000080cfc0300006c6c6c110874f00a' '2008000500a81e2b2b0000000000000000000000000000000000000016361636' '1636163600002b0c2b0c2b0c2b0c000000000000000000000000000000000000' '000000000000000000000000000000000000000000edb5ce0000000000c24da7' '1111010100000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' '000000000000000000000000000000000000000000000000000000000000de27' '0000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' '029e00000000000000434d5233324758344d32433333333343313620200080ce' '0000000000000000000000000000000000000000000000000000000000000000' '0c4a01200000000000a3000005fc3f04004d575710ac03f00a2008000500b022' '2c00000000000000009cceb5b5b5e7e700000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' ) # clear the part number; the Vengeance RGB sample already didn't set the TS bit _NON_TS_SPD = patch_spd_dump(_VENGEANCE_RGB_SAMPLE, slice(0x149, 0x15d), b' ' * 20) # set the TS bit _TS_SPD = patch_spd_dump(_NON_TS_SPD, 0x0e, 0x80) # DDR4 SPD decoding @pytest.fixture def cmr_spd(): return Ddr4Spd(_VENGEANCE_RGB_SAMPLE) def test_spd_bytes_used(cmr_spd): assert cmr_spd.spd_bytes_used == 384 def test_spd_bytes_total(cmr_spd): assert cmr_spd.spd_bytes_total == 512 def test_spd_revision(cmr_spd): assert cmr_spd.spd_revision == (1, 0) def test_dram_device_type(cmr_spd): assert cmr_spd.dram_device_type == Ddr4Spd.DramDeviceType.DDR4_SDRAM def test_module_type(cmr_spd): assert cmr_spd.module_type == (Ddr4Spd.BaseModuleType.UDIMM, None) def test_module_thermal_sensor(cmr_spd): assert not cmr_spd.module_thermal_sensor def test_module_manufacturer(cmr_spd): assert cmr_spd.module_manufacturer == 'Corsair' def test_module_part_number(cmr_spd): assert cmr_spd.module_part_number == 'CMR32GX4M2C3333C16' def test_dram_manufacturer(cmr_spd): assert cmr_spd.dram_manufacturer == 'Samsung' @pytest.fixture def smbus(): smbus = VirtualSmbus(parent_driver='i801_smbus') # hack: clear all spd addresses for address in range(0x50, 0x58): smbus._data[address] = None return smbus # DDR4 modules using a TSE2004-compatible SPD EEPROM with temperature sensor def test_tse2004_ignores_not_allowed_buses(smbus, monkeypatch): smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) checks = [ ('parent_driver', 'other'), ] for attr, val in checks: with monkeypatch.context() as m: m.setattr(smbus, attr, val) assert list(Ddr4Temperature.probe(smbus)) == [], \ f'changing {attr} did not cause a mismatch' def test_tse2004_does_not_match_non_ee1004_device(smbus): smbus.emulate_eeprom_at(0x51, 'other', _TS_SPD) assert list(map(type, Ddr4Temperature.probe(smbus))) == [] def test_tse2004_does_not_match_non_ts_devices(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _NON_TS_SPD) assert list(map(type, Ddr4Temperature.probe(smbus))) == [] def test_tse2004_finds_ts_devices(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) smbus.emulate_eeprom_at(0x53, 'ee1004', _TS_SPD) smbus.emulate_eeprom_at(0x55, 'ee1004', _TS_SPD) smbus.emulate_eeprom_at(0x57, 'ee1004', _TS_SPD) devs = list(Ddr4Temperature.probe(smbus)) assert list(map(type, devs)) == [Ddr4Temperature] * 4 assert devs[1].description == 'Corsair DIMM4 (experimental)' def test_tse2004_get_status_is_unsafe(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) dimm = next(Ddr4Temperature.probe(smbus)) assert dimm.get_status() == [] def test_tse2004_get_status_reads_temperature(smbus): enable = ['smbus', 'ddr4_temperature'] smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) dimm = next(Ddr4Temperature.probe(smbus)) with dimm.connect(unsafe=enable): smbus.write_block_data(0x19, 0x05, 0xe19c) status = dimm.get_status(unsafe=enable) expected = [ ('Temperature', 25.75, '°C'), ] assert status == expected def test_tse2004_get_status_reads_negative_temperature(smbus): enable = ['smbus', 'ddr4_temperature'] smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) dimm = next(Ddr4Temperature.probe(smbus)) with dimm.connect(unsafe=enable): smbus.write_block_data(0x19, 0x05, 0x1e74) status = dimm.get_status(unsafe=enable) expected = [ ('Temperature', -24.75, '°C'), ] assert status == expected # Corsair Vengeance RGB @pytest.fixture def vengeance_rgb(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _VENGEANCE_RGB_SAMPLE) dimm = next(VengeanceRgb.probe(smbus)) smbus.open() for reg in range(256): smbus.write_byte_data(0x59, reg, 0xba) smbus.close() return (smbus, dimm) def test_vengeance_rgb_finds_devices(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _VENGEANCE_RGB_SAMPLE) smbus.emulate_eeprom_at(0x53, 'ee1004', _VENGEANCE_RGB_SAMPLE) smbus.emulate_eeprom_at(0x55, 'ee1004', _VENGEANCE_RGB_SAMPLE) smbus.emulate_eeprom_at(0x57, 'ee1004', _VENGEANCE_RGB_SAMPLE) devs = list(VengeanceRgb.probe(smbus)) assert list(map(type, devs)) == [VengeanceRgb] * 4 assert devs[1].description == 'Corsair Vengeance RGB DIMM4 (experimental)' def test_vengeance_get_status_reads_temperature(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb def forbid(*args, **kwargs): assert False, 'should not reach here' smbus.read_block_data = forbid with dimm.connect(unsafe=enable): smbus.write_word_data(0x19, 0x05, 0x9ce1) status = dimm.get_status(unsafe=enable) expected = [ ('Temperature', 25.75, '°C'), ] assert status == expected def test_vengeance_rgb_set_color_is_unsafe(vengeance_rgb): _, dimm = vengeance_rgb with pytest.raises(UnsafeFeaturesNotEnabled): assert dimm.set_color('led', 'off', []) with pytest.raises(UnsafeFeaturesNotEnabled): assert dimm.set_color('led', 'off', [], unsafe='vengeance_rgb') with pytest.raises(UnsafeFeaturesNotEnabled): assert dimm.set_color('led', 'off', [], unsafe='smbus') def test_vengeance_rgb_asserts_rgb_address_validity(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): smbus.write_byte_data(0x59, 0xa6, 0x00) with pytest.raises(ExpectationNotMet): dimm.set_color('led', 'off', [], unsafe=enable) def test_vengeance_rgb_sets_color_to_off(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): dimm.set_color('led', 'off', [], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x00 assert smbus.read_byte_data(0x59, 0xa6) == 0x00 assert smbus.read_byte_data(0x59, 0xa7) == 0x01 for color_component in range(0xb0, 0xb3): assert smbus.read_byte_data(0x59, color_component) == 0x00 def test_vengeance_rgb_sets_color_to_fixed(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] dimm.set_color('led', 'fixed', [radical_red], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x00 assert smbus.read_byte_data(0x59, 0xa6) == 0x00 assert smbus.read_byte_data(0x59, 0xa7) == 0x01 assert smbus.read_byte_data(0x59, 0xb0) == 0xff assert smbus.read_byte_data(0x59, 0xb1) == 0x35 assert smbus.read_byte_data(0x59, 0xb2) == 0x5e def test_vengeance_rgb_sets_color_to_breathing(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] dimm.set_color('led', 'breathing', [radical_red, mountain_meadow], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x20 assert smbus.read_byte_data(0x59, 0xa5) == 0x20 assert smbus.read_byte_data(0x59, 0xa6) == 0x02 assert smbus.read_byte_data(0x59, 0xa7) == 0x02 assert smbus.read_byte_data(0x59, 0xb0) == 0xff assert smbus.read_byte_data(0x59, 0xb1) == 0x35 assert smbus.read_byte_data(0x59, 0xb2) == 0x5e assert smbus.read_byte_data(0x59, 0xb3) == 0x1a assert smbus.read_byte_data(0x59, 0xb4) == 0xb3 assert smbus.read_byte_data(0x59, 0xb5) == 0x85 def test_vengeance_rgb_sets_single_color_to_breathing(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] dimm.set_color('led', 'breathing', [radical_red], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x20 assert smbus.read_byte_data(0x59, 0xa5) == 0x20 assert smbus.read_byte_data(0x59, 0xa6) == 0x00 # special case assert smbus.read_byte_data(0x59, 0xa7) == 0x01 assert smbus.read_byte_data(0x59, 0xb0) == 0xff assert smbus.read_byte_data(0x59, 0xb1) == 0x35 assert smbus.read_byte_data(0x59, 0xb2) == 0x5e def test_vengeance_rgb_sets_color_to_fading(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] dimm.set_color('led', 'fading', [radical_red, mountain_meadow], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x20 assert smbus.read_byte_data(0x59, 0xa5) == 0x20 assert smbus.read_byte_data(0x59, 0xa6) == 0x01 assert smbus.read_byte_data(0x59, 0xa7) == 0x02 assert smbus.read_byte_data(0x59, 0xb0) == 0xff assert smbus.read_byte_data(0x59, 0xb1) == 0x35 assert smbus.read_byte_data(0x59, 0xb2) == 0x5e assert smbus.read_byte_data(0x59, 0xb3) == 0x1a assert smbus.read_byte_data(0x59, 0xb4) == 0xb3 assert smbus.read_byte_data(0x59, 0xb5) == 0x85 def test_vengeance_rgb_animation_speed_presets_set_correct_timings(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] presets = ['slowest', 'slower', 'normal', 'faster', 'fastest'] timings = [0x3f, 0x30, 0x20, 0x10, 0x01] for preset, timing in zip(presets, timings): dimm.set_color('led', 'fading', [radical_red, mountain_meadow], speed=preset, unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == timing, f'wrong tp1 for {preset!r}' assert smbus.read_byte_data(0x59, 0xa5) == timing, f'wrong tp2 for {preset!r}' def test_vengeance_rgb_animation_transition_ticks_overrides_tp1(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] dimm.set_color('led', 'fading', [radical_red, mountain_meadow], transition_ticks=0x01, unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x01 assert smbus.read_byte_data(0x59, 0xa5) == 0x20 def test_vengeance_rgb_animation_transition_ticks_overrides_tp2(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] dimm.set_color('led', 'fading', [radical_red, mountain_meadow], stable_ticks=0x01, unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x20 assert smbus.read_byte_data(0x59, 0xa5) == 0x01 liquidctl-1.5.1/tests/test_hidapi_device.py000066400000000000000000000106001401367561700210470ustar00rootroot00000000000000from pytest import fixture from liquidctl.driver.usb import HidapiDevice class _mockhidapi: @staticmethod def device(): return _mockdevice() class _mockdevice: pass _SAMPLE_HID_INFO = { 'path': b'path', 'vendor_id': 0xf001, 'product_id': 0xf002, 'serial_number': 'serial number', 'release_number': 0xf003, 'manufacturer_string': 'manufacturer', 'product_string': 'product', 'usage_page': 0xf004, 'usage': 0xf005, 'interface_number': 0x01, } @fixture def dev(): return HidapiDevice(_mockhidapi, _SAMPLE_HID_INFO) def test_opens(dev, monkeypatch): opened = False def _open_path(path): assert isinstance(path, bytes) nonlocal opened opened = True monkeypatch.setattr(dev.hiddev, 'open_path', _open_path, raising=False) dev.open() assert opened def test_closes(dev, monkeypatch): opened = True def _close(): nonlocal opened opened = False monkeypatch.setattr(dev.hiddev, 'close', _close, raising=False) dev.close() assert not opened def test_can_clear_enqueued_reports(dev, monkeypatch): queue = [[1], [2], [3]] def _set_nonblocking(v): assert isinstance(v, int) return 0 def _read(max_length, timeout_ms=0): assert isinstance(max_length, int) assert isinstance(timeout_ms, int) assert timeout_ms == 0, 'use hid_read' nonlocal queue if queue: return queue.pop() return [] monkeypatch.setattr(dev.hiddev, 'set_nonblocking', _set_nonblocking, raising=False) monkeypatch.setattr(dev.hiddev, 'read', _read, raising=False) dev.clear_enqueued_reports() assert not queue def test_can_clear_enqueued_reports_without_nonblocking(dev, monkeypatch): queue = [[1], [2], [3]] def _set_nonblocking(v): assert isinstance(v, int) return -1 def _read(max_length, timeout_ms=0): assert isinstance(max_length, int) assert isinstance(timeout_ms, int) assert timeout_ms > 0, 'use hid_read_timeout' nonlocal queue if queue: return queue.pop() return [] monkeypatch.setattr(dev.hiddev, 'set_nonblocking', _set_nonblocking, raising=False) monkeypatch.setattr(dev.hiddev, 'read', _read, raising=False) dev.clear_enqueued_reports() assert not queue def test_reads(dev, monkeypatch): def _set_nonblocking(v): assert isinstance(v, int) return 0 def _read(max_length, timeout_ms=0): assert isinstance(max_length, int) assert isinstance(timeout_ms, int) assert timeout_ms == 0, 'use hid_read' return [0xff] + [0]*(max_length - 1) # report ID is part of max_length *if present* monkeypatch.setattr(dev.hiddev, 'set_nonblocking', _set_nonblocking, raising=False) monkeypatch.setattr(dev.hiddev, 'read', _read, raising=False) assert dev.read(5) == [0xff, 0, 0, 0, 0] def test_can_write(dev, monkeypatch): def _write(buff): buff = bytes(buff) return len(buff) # report ID is (always) part of returned length monkeypatch.setattr(dev.hiddev, 'write', _write, raising=False) assert dev.write([0xff, 42]) == 2 assert dev.write([0, 42]) == 2 assert dev.write(b'foo') == 3 def test_gets_feature_report(dev, monkeypatch): def _get(report_num, max_length): assert isinstance(report_num, int) assert isinstance(max_length, int) return [report_num] + [0]*(max_length - 1) # report ID is (always) part of max_length monkeypatch.setattr(dev.hiddev, 'get_feature_report', _get, raising=False) assert dev.get_feature_report(0xff, 3) == [0xff, 0, 0] assert dev.get_feature_report(0, 3) == [0, 0, 0] def test_can_send_feature_report(dev, monkeypatch): def _send(buff): buff = bytes(buff) return len(buff) # report ID is (always) part of returned length monkeypatch.setattr(dev.hiddev, 'send_feature_report', _send, raising=False) assert dev.send_feature_report([0xff, 42]) == 2 assert dev.send_feature_report([0, 42]) == 2 assert dev.send_feature_report(b'foo') == 3 def test_exposes_unified_properties(dev): assert dev.vendor_id == 0xf001 assert dev.product_id == 0xf002 assert dev.release_number == 0xf003 assert dev.serial_number == 'serial number' assert dev.bus == 'hid' assert dev.address == 'path' assert dev.port is None liquidctl-1.5.1/tests/test_hydro_platinum.py000066400000000000000000000254041401367561700213400ustar00rootroot00000000000000import pytest from liquidctl.driver.hydro_platinum import HydroPlatinum from liquidctl.pmbus import compute_pec from _testutils import MockHidapiDevice, Report, MockRuntimeStorage _SAMPLE_PATH = (r'IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC@14/XH' r'C@14000000/HS11@14a00000/USB2.0 Hub@14a00000/AppleUSB20InternalH' r'ub@14a00000/AppleUSB20HubPort@14a10000/USB2.0 Hub@14a10000/Apple' r'USB20Hub@14a10000/AppleUSB20HubPort@14a12000/H100i Platinum@14a1' r'2000/IOUSBHostInterface@0/AppleUserUSBHostHIDDevice+Win\\#!&3142') _WIN_MAX_PATH = 260 # Windows API should be the bottleneck @pytest.fixture def h115iPlatinumDevice(): description = 'Mock H115i Platinum' kwargs = {'fan_count': 2, 'fan_leds': 4} device = _MockHydroPlatinumDevice() dev = HydroPlatinum(device, description, **kwargs) runtime_storage = MockRuntimeStorage(key_prefixes='testing') runtime_storage.store('leds_enabled', 0) dev.connect(runtime_storage=runtime_storage) return dev @pytest.fixture def h100iPlatinumSeDevice(): description = 'Mock H100i Platinum SE' kwargs = {'fan_count': 2, 'fan_leds': 16} device = _MockHydroPlatinumDevice() dev = HydroPlatinum(device, description, **kwargs) runtime_storage = MockRuntimeStorage(key_prefixes='testing') runtime_storage.store('leds_enabled', 0) dev.connect(runtime_storage=runtime_storage) return dev class _MockHydroPlatinumDevice(MockHidapiDevice): def __init__(self): super().__init__(vendor_id=0xffff, product_id=0x0c17, address=_SAMPLE_PATH) self.fw_version = (1, 1, 15) self.temperature = 30.9 self.fan1_speed = 1499 self.fan2_speed = 1512 self.pump_speed = 2702 def read(self, length): pre = super().read(length) if pre: return pre buf = bytearray(64) buf[2] = self.fw_version[0] << 4 | self.fw_version[1] buf[3] = self.fw_version[2] buf[7] = int((self.temperature - int(self.temperature)) * 255) buf[8] = int(self.temperature) buf[15:17] = self.fan1_speed.to_bytes(length=2, byteorder='little') buf[22:24] = self.fan2_speed.to_bytes(length=2, byteorder='little') buf[29:31] = self.pump_speed.to_bytes(length=2, byteorder='little') buf[-1] = compute_pec(buf[1:-1]) return buf[:length] def test_h115i_platinum_device_connect(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.disconnect() # the fixture had by default connected to the device def mock_open(): nonlocal opened opened = True dev.device.open = mock_open opened = False with dev.connect() as cm: assert cm == dev assert opened def test_h115i_platinum_device_command_format(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.initialize() dev.get_status() dev.set_fixed_speed(channel='fan', duty=100) dev.set_speed_profile(channel='fan', profile=[]) dev.set_color(channel='led', mode='off', colors=[]) assert len(dev.device.sent) == 9 for i, (report, data) in enumerate(dev.device.sent): assert report == 0 assert len(data) == 64 assert data[0] == 0x3f assert data[1] >> 3 == i + 1 assert data[-1] == compute_pec(data[1:-1]) def test_h115i_platinum_device_command_format_enabled(h115iPlatinumDevice): dev = h115iPlatinumDevice # test that the led enable messages are not sent if they are sent again dev.initialize() dev._data.store('leds_enabled', 1) dev.get_status() dev.set_fixed_speed(channel='fan', duty=100) dev.set_speed_profile(channel='fan', profile=[]) dev.set_color(channel='led', mode='off', colors=[]) assert len(dev.device.sent) == 6 for i, (report, data) in enumerate(dev.device.sent): assert report == 0 assert len(data) == 64 assert data[0] == 0x3f assert data[1] >> 3 == i + 1 assert data[-1] == compute_pec(data[1:-1]) def test_h115i_platinum_device_get_status(h115iPlatinumDevice): dev = h115iPlatinumDevice temp, fan1, fan2, pump = dev.get_status() assert temp[1] == pytest.approx(dev.device.temperature, abs=1 / 255) assert fan1[1] == dev.device.fan1_speed assert fan2[1] == dev.device.fan2_speed assert pump[1] == dev.device.pump_speed assert dev.device.sent[0].data[1] & 0b111 == 0 assert dev.device.sent[0].data[2] == 0xff def test_h115i_platinum_device_handle_real_statuses(h115iPlatinumDevice): dev = h115iPlatinumDevice samples = [ ( 'ff08110f0001002c1e0000aee803aed10700aee803aece0701aa0000aa9c0900' '0000000000000000000000000000000000000000000000000000000000000010' ), ( 'ff40110f009e14011b0102ffe8037e6a0502ffe8037e6d0501aa0000aa350901' '0000000000000000000000000000000000000000000000000000000000000098' ) ] for sample in samples: dev.device.preload_read(Report(0, bytes.fromhex(sample))) status = dev.get_status() assert len(status) == 4 assert status[0][1] != dev.device.temperature def test_h115i_platinum_device_initialize_status(h115iPlatinumDevice): dev = h115iPlatinumDevice dev._data.store('leds_enabled', 1) (fw_version, ) = dev.initialize() assert fw_version[1] == '%d.%d.%d' % dev.device.fw_version assert dev._data.load('leds_enabled', of_type=int, default=1) == 0 def test_h115i_platinum_device_common_cooling_prefix(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.initialize(pump_mode='extreme') dev.set_fixed_speed(channel='fan', duty=42) dev.set_speed_profile(channel='fan', profile=[(20, 0), (55, 100)]) assert len(dev.device.sent) == 3 for _, data in dev.device.sent: assert data[0x1] & 0b111 == 0 assert data[0x2] == 0x14 # opaque but apparently important prefix (see @makk50's comments in #82): assert data[0x3:0xb] == [0x0, 0xff, 0x5] + 5 * [0xff] def test_h115i_platinum_device_set_pump_mode(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.initialize(pump_mode='extreme') assert dev.device.sent[0].data[0x17] == 0x2 with pytest.raises(KeyError): dev.initialize(pump_mode='invalid') def test_h115i_platinum_device_fixed_fan_speeds(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.set_fixed_speed(channel='fan', duty=42) dev.set_fixed_speed(channel='fan1', duty=84) assert dev.device.sent[-1].data[0x0b] == 0x2 assert dev.device.sent[-1].data[0x10] / 2.55 == pytest.approx(84, abs=1 / 2.55) assert dev.device.sent[-1].data[0x11] == 0x2 assert dev.device.sent[-1].data[0x16] / 2.55 == pytest.approx(42, abs=1 / 2.55) with pytest.raises(ValueError): dev.set_fixed_speed('invalid', 0) def test_h115i_platinum_device_custom_fan_profiles(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.set_speed_profile(channel='fan', profile=iter([(20, 0), (55, 100)])) dev.set_speed_profile(channel='fan1', profile=iter([(30, 20), (50, 80)])) assert dev.device.sent[-1].data[0x0b] == 0x0 assert dev.device.sent[-1].data[0x1d] == 7 assert dev.device.sent[-1].data[0x1e:0x2c] == [30, 51, 50, 204] + 5 * [60, 255] assert dev.device.sent[-1].data[0x11] == 0x0 assert dev.device.sent[-1].data[0x2c:0x3a] == [20, 0, 55, 255] + 5 * [60, 255] with pytest.raises(ValueError): dev.set_speed_profile('invalid', []) with pytest.raises(ValueError): dev.set_speed_profile('fan', zip(range(10), range(10))) def test_h115i_platinum_device_address_leds(h115iPlatinumDevice): dev = h115iPlatinumDevice colors = [[i + 3, i + 2, i + 1] for i in range(0, 24 * 3, 3)] encoded = list(range(1, 24 * 3 + 1)) dev.set_color(channel='led', mode='super-fixed', colors=iter(colors)) assert len(dev.device.sent) == 5 # 3 for enable, 2 for off assert dev.device.sent[0].data[1] & 0b111 == 0b001 assert dev.device.sent[1].data[1] & 0b111 == 0b010 assert dev.device.sent[2].data[1] & 0b111 == 0b011 assert dev.device.sent[3].data[1] & 0b111 == 0b100 assert dev.device.sent[3].data[2:62] == encoded[:60] assert dev.device.sent[4].data[1] & 0b111 == 0b101 assert dev.device.sent[4].data[2:14] == encoded[60:] def test_h100i_platinum_se_device_address_leds(h100iPlatinumSeDevice): dev = h100iPlatinumSeDevice colors = [[i + 3, i + 2, i + 1] for i in range(0, 48 * 3, 3)] encoded = list(range(1, 48 * 3 + 1)) dev.set_color(channel='led', mode='super-fixed', colors=iter(colors)) assert len(dev.device.sent) == 6 # 3 for enable, 3 for the leds assert dev.device.sent[0].data[1] & 0b111 == 0b001 assert dev.device.sent[1].data[1] & 0b111 == 0b010 assert dev.device.sent[2].data[1] & 0b111 == 0b011 assert dev.device.sent[3].data[1] & 0b111 == 0b100 assert dev.device.sent[3].data[2:62] == encoded[:60] assert dev.device.sent[4].data[1] & 0b111 == 0b101 assert dev.device.sent[4].data[2:62] == encoded[60:120] assert dev.device.sent[5].data[1] & 0b111 == 0b110 assert dev.device.sent[5].data[2:26] == encoded[120:] def test_h115i_platinum_device_synchronize(h115iPlatinumDevice): dev = h115iPlatinumDevice colors = [[3, 2, 1]] encoded = [1, 2, 3] * 24 dev.set_color(channel='led', mode='fixed', colors=iter(colors)) assert len(dev.device.sent) == 5 # 3 for enable, 2 for off assert dev.device.sent[0].data[1] & 0b111 == 0b001 assert dev.device.sent[1].data[1] & 0b111 == 0b010 assert dev.device.sent[2].data[1] & 0b111 == 0b011 assert dev.device.sent[3].data[1] & 0b111 == 0b100 assert dev.device.sent[3].data[2:62] == encoded[:60] assert dev.device.sent[4].data[1] & 0b111 == 0b101 assert dev.device.sent[4].data[2:14] == encoded[60:] def test_h115i_platinum_device_leds_off(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.set_color(channel='led', mode='off', colors=iter([])) assert len(dev.device.sent) == 5 # 3 for enable, 2 for off for _, data in dev.device.sent[3:5]: assert data[2:62] == [0] * 60 def test_h115i_platinum_device_invalid_color_modes(h115iPlatinumDevice): dev = h115iPlatinumDevice with pytest.raises(ValueError): dev.set_color('led', 'invalid', []) with pytest.raises(ValueError): dev.set_color('invalid', 'off', []) assert len(dev.device.sent) == 0 def test_h115i_platinum_device_short_enough_storage_path(): description = 'Mock H115i Platinum' kwargs = {'fan_count': 2, 'fan_leds': 4} device = _MockHydroPlatinumDevice() dev = HydroPlatinum(device, description, **kwargs) dev.connect() assert len(dev._data._backend._write_dir) < _WIN_MAX_PATH assert dev._data._backend._write_dir.endswith('3142') def test_h115i_platinum_device_bad_stored_data(h115iPlatinumDevice): h115iPlatinumDevice # TODO pass liquidctl-1.5.1/tests/test_keyval.py000066400000000000000000000010631401367561700175700ustar00rootroot00000000000000from pathlib import Path import pytest def test_fs_backend_handles_values_corupted_with_nulls(tmpdir, caplog): from liquidctl.keyval import _FilesystemBackend run_dir = tmpdir.mkdir('run_dir') store = _FilesystemBackend(key_prefixes=['prefix'], runtime_dirs=[run_dir]) store.store('key', 42) key_file = Path(run_dir).joinpath('prefix', 'key') assert key_file.read_bytes() == b'42', 'unit test is unsound' key_file.write_bytes(b'\x00') val = store.load('key') assert val is None assert 'was corrupted' in caplog.text liquidctl-1.5.1/tests/test_kraken2.py000066400000000000000000000103621401367561700176340ustar00rootroot00000000000000import pytest from liquidctl.driver.kraken2 import Kraken2 from _testutils import MockHidapiDevice from liquidctl.error import NotSupportedByDevice @pytest.fixture def mockKrakenXDevice(): device = _MockKrakenDevice(fw_version=(6, 0, 2)) dev = Kraken2(device, 'Mock X62', device_type=Kraken2.DEVICE_KRAKENX) dev.connect() return dev @pytest.fixture def mockOldKrakenXDevice(): device = _MockKrakenDevice(fw_version=(2, 5, 8)) dev = Kraken2(device, 'Mock X62', device_type=Kraken2.DEVICE_KRAKENX) dev.connect() return dev @pytest.fixture def mockKrakenMDevice(): device = _MockKrakenDevice(fw_version=(6, 0, 2)) dev = Kraken2(device, 'Mock M22', device_type=Kraken2.DEVICE_KRAKENM) dev.connect() return dev class _MockKrakenDevice(MockHidapiDevice): def __init__(self, fw_version): super().__init__(vendor_id=0xffff, product_id=0x1e71) self.fw_version = fw_version self.temperature = 30.9 self.fan_speed = 1499 self.pump_speed = 2702 def read(self, length): pre = super().read(length) if pre: return pre buf = bytearray(64) buf[1:3] = divmod(int(self.temperature * 10), 10) buf[3:5] = self.fan_speed.to_bytes(length=2, byteorder='big') buf[5:7] = self.pump_speed.to_bytes(length=2, byteorder='big') major, minor, patch = self.fw_version buf[0xb] = major buf[0xc:0xe] = minor.to_bytes(length=2, byteorder='big') buf[0xe] = patch return buf[:length] def test_kraken_connect(mockKrakenXDevice): def mock_open(): nonlocal opened opened = True mockKrakenXDevice.device.open = mock_open opened = False with mockKrakenXDevice.connect() as cm: assert cm == mockKrakenXDevice assert opened def test_kraken_get_status(mockKrakenXDevice): fan, fw_ver, temp, pump = sorted(mockKrakenXDevice.get_status()) assert fw_ver[1] == '6.0.2' assert temp[1] == pytest.approx(mockKrakenXDevice.device.temperature) assert fan[1] == mockKrakenXDevice.device.fan_speed assert pump[1] == mockKrakenXDevice.device.pump_speed def test_kraken_not_totally_broken(mockKrakenXDevice): """Reasonable example calls to untested APIs do not raise exceptions.""" dev = mockKrakenXDevice dev.initialize() dev.set_color(channel='ring', mode='loading', colors=iter([[90, 80, 0]]), speed='slowest') dev.set_speed_profile(channel='fan', profile=iter([(20, 20), (30, 40), (40, 100)])) dev.set_fixed_speed(channel='pump', duty=50) dev.set_instantaneous_speed(channel='pump', duty=50) def test_kraken_set_fixed_speeds(mockOldKrakenXDevice): mockOldKrakenXDevice.set_fixed_speed(channel='fan', duty=42) mockOldKrakenXDevice.set_fixed_speed(channel='pump', duty=84) fan_report, pump_report = mockOldKrakenXDevice.device.sent assert fan_report.number == 2 assert fan_report.data[0:4] == [0x4d, 0, 0, 42] assert pump_report.number == 2 assert pump_report.data[0:4] == [0x4d, 0x40, 0, 84] def test_kraken_speed_profiles_not_supported(mockOldKrakenXDevice): with pytest.raises(NotSupportedByDevice): mockOldKrakenXDevice.set_speed_profile('fan', [(20, 42)]) with pytest.raises(NotSupportedByDevice): mockOldKrakenXDevice.set_speed_profile('pump', [(20, 84)]) def test_krakenM_get_status(mockKrakenMDevice): (fw_ver,) = mockKrakenMDevice.get_status() assert fw_ver[1] == '6.0.2' def test_krakenM_speed_control_not_supported(mockKrakenMDevice): with pytest.raises(NotSupportedByDevice): mockKrakenMDevice.set_fixed_speed('fan', 42) with pytest.raises(NotSupportedByDevice): mockKrakenMDevice.set_fixed_speed('pump', 84) with pytest.raises(NotSupportedByDevice): mockKrakenMDevice.set_speed_profile('fan', [(20, 42)]) with pytest.raises(NotSupportedByDevice): mockKrakenMDevice.set_speed_profile('pump', [(20, 84)]) def test_krakenM_not_totally_broken(mockKrakenMDevice): """Reasonable example calls to untested APIs do not raise exceptions.""" dev = mockKrakenMDevice dev.initialize() dev.set_color(channel='ring', mode='loading', colors=iter([[90, 80, 0]]), speed='slowest') liquidctl-1.5.1/tests/test_kraken3.py000066400000000000000000000072061401367561700176400ustar00rootroot00000000000000import pytest from liquidctl.driver.kraken3 import KrakenX3, KrakenZ3 from liquidctl.driver.kraken3 import _COLOR_CHANNELS_KRAKENX from liquidctl.driver.kraken3 import _SPEED_CHANNELS_KRAKENX from liquidctl.driver.kraken3 import _SPEED_CHANNELS_KRAKENZ from liquidctl.util import Hue2Accessory from liquidctl.util import HUE2_MAX_ACCESSORIES_IN_CHANNEL as MAX_ACCESSORIES from _testutils import MockHidapiDevice, Report # https://github.com/liquidctl/liquidctl/issues/160#issuecomment-664044103 _SAMPLE_STATUS = bytes.fromhex( '7502200036000b51535834353320012101a80635350000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' ) # https://github.com/liquidctl/liquidctl/issues/160#issue-665781804 _FAULTY_STATUS = bytes.fromhex( '7502200036000b5153583435332001ffffcc0a64640000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' ) @pytest.fixture def mockKrakenXDevice(): device = _MockKrakenDevice(raw_led_channels=len(_COLOR_CHANNELS_KRAKENX) - 1) dev = KrakenX3(device, 'Corsair Kraken X73', speed_channels=_SPEED_CHANNELS_KRAKENX, color_channels=_COLOR_CHANNELS_KRAKENX) dev.connect() return dev @pytest.fixture def mockKrakenZDevice(): device = _MockKrakenDevice(raw_led_channels=0) dev = KrakenZ3(device, 'Mock Kraken Z73', speed_channels=_SPEED_CHANNELS_KRAKENZ, color_channels={}) dev.connect() return dev class _MockKrakenDevice(MockHidapiDevice): def __init__(self, raw_led_channels): super().__init__() self.raw_led_channels = raw_led_channels def write(self, data): reply = bytearray(64) if data[0:2] == [0x10, 0x01]: reply[0:2] = [0x11, 0x01] elif data[0:2] == [0x20, 0x03]: reply[0:2] = [0x21, 0x03] reply[14] = self.raw_led_channels if self.raw_led_channels > 1: reply[15 + 1 * MAX_ACCESSORIES] = Hue2Accessory.KRAKENX_GEN4_RING.value reply[15 + 2 * MAX_ACCESSORIES] = Hue2Accessory.KRAKENX_GEN4_LOGO.value self.preload_read(Report(0, reply)) def test_kracken_x_device_parses_status_fields(mockKrakenXDevice): mockKrakenXDevice.device.preload_read(Report(0, _SAMPLE_STATUS)) temperature, pump_speed, pump_duty = mockKrakenXDevice.get_status() assert temperature == ('Liquid temperature', 33.1, '°C') assert pump_speed == ('Pump speed', 1704, 'rpm') assert pump_duty == ('Pump duty', 53, '%') def test_kracken_x_device_warns_if_faulty_temperature(mockKrakenXDevice, caplog): mockKrakenXDevice.device.preload_read(Report(0, _FAULTY_STATUS)) mockKrakenXDevice.get_status() assert 'unexpected temperature reading' in caplog.text def test_kracken_x_device_not_totally_broken(mockKrakenXDevice): """Reasonable example calls to untested APIs do not raise exceptions.""" dev = mockKrakenXDevice dev.initialize() dev.set_color(channel='ring', mode='fixed', colors=iter([[3, 2, 1]]), speed='fastest') dev.set_speed_profile(channel='pump', profile=iter([(20, 20), (30, 50), (40, 100)])) dev.set_fixed_speed(channel='pump', duty=50) def test_kracken_z_device_not_totally_broken(mockKrakenZDevice): """Reasonable example calls to untested APIs do not raise exceptions.""" dev = mockKrakenZDevice dev.initialize() dev.device.preload_read(Report(0, _SAMPLE_STATUS)) dev.get_status() dev.set_speed_profile(channel='fan', profile=iter([(20, 20), (30, 50), (40, 100)])) dev.set_fixed_speed(channel='pump', duty=50) liquidctl-1.5.1/tests/test_nvidia.py000066400000000000000000000305401401367561700175510ustar00rootroot00000000000000from liquidctl.driver.nvidia import * from liquidctl.error import * import pytest from _testutils import VirtualSmbus # EVGA Pascal @pytest.fixture def evga_1080_ftw_bus(): return VirtualSmbus( description='NVIDIA i2c adapter 1 at 1:00.0', parent_vendor=NVIDIA, parent_device=NVIDIA_GTX_1080, parent_subsystem_vendor=EVGA, parent_subsystem_device=EVGA_GTX_1080_FTW, parent_driver='nvidia', ) def test_evga_pascal_finds_devices(evga_1080_ftw_bus, monkeypatch): smbus = evga_1080_ftw_bus checks = [ ('parent_subsystem_vendor', 0xffff), ('parent_vendor', 0xffff), ('parent_driver', 'other'), ('parent_subsystem_device', 0xffff), ('parent_device', 0xffff), ('description', 'NVIDIA i2c adapter 2 at 1:00.0'), ] for attr, val in checks: with monkeypatch.context() as m: m.setattr(smbus, attr, val) assert list(EvgaPascal.probe(smbus)) == [], \ f'changing {attr} did not cause a mismatch' assert list(map(type, EvgaPascal.probe(smbus))) == [EvgaPascal] def test_evga_pascal_get_status_is_noop(evga_1080_ftw_bus): card = next(EvgaPascal.probe(evga_1080_ftw_bus)) assert card.get_status() == [] def test_evga_pascal_get_verbose_status_is_unsafe(evga_1080_ftw_bus): card = next(EvgaPascal.probe(evga_1080_ftw_bus)) assert card.get_status(verbose=True) == [] assert card.get_status(verbose=True, unsafe='other') == [] def test_evga_pascal_gets_verbose_status(evga_1080_ftw_bus): enable = ['smbus'] card = next(EvgaPascal.probe(evga_1080_ftw_bus)) with card.connect(unsafe=enable): evga_1080_ftw_bus.write_byte_data(0x49, 0x09, 0xaa) evga_1080_ftw_bus.write_byte_data(0x49, 0x0a, 0xbb) evga_1080_ftw_bus.write_byte_data(0x49, 0x0b, 0xcc) evga_1080_ftw_bus.write_byte_data(0x49, 0x0c, 0x01) status = card.get_status(verbose=True, unsafe=enable) expected = [ ('Mode', EvgaPascal.Mode.FIXED, ''), ('Color', 'aabbcc', ''), ] assert status == expected def test_evga_pascal_set_color_is_unsafe(evga_1080_ftw_bus): card = next(EvgaPascal.probe(evga_1080_ftw_bus)) with pytest.raises(UnsafeFeaturesNotEnabled): card.set_color('led', 'off', []) with pytest.raises(UnsafeFeaturesNotEnabled): card.set_color('led', 'off', [], unsafe='other') def test_evga_pascal_sets_color_to_off(evga_1080_ftw_bus): enable = ['smbus'] card = next(EvgaPascal.probe(evga_1080_ftw_bus)) with card.connect(unsafe=enable): # change mode register to something other than 0 (=off) evga_1080_ftw_bus.write_byte_data(0x49, 0x0c, 0x01) card.set_color('led', 'off', [], unsafe=enable) assert evga_1080_ftw_bus.read_byte_data(0x49, 0x0c) == 0x00 # persistence not enabled assert evga_1080_ftw_bus.read_byte_data(0x49, 0x23) == 0x00 def test_evga_pascal_sets_color_to_fixed(evga_1080_ftw_bus): enable = ['smbus'] card = next(EvgaPascal.probe(evga_1080_ftw_bus)) with card.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] card.set_color('led', 'fixed', [radical_red], unsafe=enable) assert evga_1080_ftw_bus.read_byte_data(0x49, 0x0c) == 0x01 assert evga_1080_ftw_bus.read_byte_data(0x49, 0x09) == 0xff assert evga_1080_ftw_bus.read_byte_data(0x49, 0x0a) == 0x35 assert evga_1080_ftw_bus.read_byte_data(0x49, 0x0b) == 0x5e # persistence not enabled assert evga_1080_ftw_bus.read_byte_data(0x49, 0x23) == 0x00 def test_evga_pascal_sets_color_to_rainbow(evga_1080_ftw_bus): enable = ['smbus'] card = next(EvgaPascal.probe(evga_1080_ftw_bus)) with card.connect(unsafe=enable): card.set_color('led', 'rainbow', [], unsafe=enable) assert evga_1080_ftw_bus.read_byte_data(0x49, 0x0c) == 0x02 # persistence not enabled assert evga_1080_ftw_bus.read_byte_data(0x49, 0x23) == 0x00 def test_evga_pascal_sets_color_to_breathing(evga_1080_ftw_bus): enable = ['smbus'] card = next(EvgaPascal.probe(evga_1080_ftw_bus)) with card.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] card.set_color('led', 'breathing', [radical_red], unsafe=enable) assert evga_1080_ftw_bus.read_byte_data(0x49, 0x0c) == 0x05 assert evga_1080_ftw_bus.read_byte_data(0x49, 0x09) == 0xff assert evga_1080_ftw_bus.read_byte_data(0x49, 0x0a) == 0x35 assert evga_1080_ftw_bus.read_byte_data(0x49, 0x0b) == 0x5e # persistence not enabled assert evga_1080_ftw_bus.read_byte_data(0x49, 0x23) == 0x00 def test_evga_pascal_sets_non_volatile_color(evga_1080_ftw_bus): enable = ['smbus'] card = next(EvgaPascal.probe(evga_1080_ftw_bus)) orig = evga_1080_ftw_bus.write_byte_data def raise_if_23h(address, register, value): orig(address, register, value) if register == 0x23: raise OSError() # mimic smbus.SMBus on actual hardware evga_1080_ftw_bus.write_byte_data = raise_if_23h with card.connect(unsafe=enable): card.set_color('led', 'off', [], non_volatile=True, unsafe=enable) assert evga_1080_ftw_bus.read_byte_data(0x49, 0x23) == 0xe5 # ASUS Turing def create_strix_2080ti_oc_bus(controller_address=None): smbus = VirtualSmbus( description='NVIDIA i2c adapter 1 at 1c:00.0', parent_vendor=NVIDIA, parent_device=NVIDIA_RTX_2080_TI_REV_A, parent_subsystem_vendor=ASUS, parent_subsystem_device=ASUS_STRIX_RTX_2080_TI_OC, parent_driver='nvidia', ) if controller_address is None: return smbus smbus.open() try: smbus.write_byte_data(controller_address, 0x20, 0x15) smbus.write_byte_data(controller_address, 0x21, 0x89) except: pass smbus.close() return smbus @pytest.fixture def strix_2080ti_oc_bus(): address = 0x2a # not the first candidate return create_strix_2080ti_oc_bus(address) def test_rog_turing_does_not_find_devices(monkeypatch): smbus = create_strix_2080ti_oc_bus() # no addresses enabled checks = [ ('parent_subsystem_vendor', 0xffff), ('parent_vendor', 0xffff), ('parent_driver', 'other'), ('parent_subsystem_device', 0xffff), ('parent_device', 0xffff), ('description', 'NVIDIA i2c adapter 2 at 1:00.0'), ] for attr, val in checks: with monkeypatch.context() as m: m.setattr(smbus, attr, val) assert list(RogTuring.probe(smbus)) == [], \ f'changing {attr} did not cause a mismatch' # with unsafe features addresses can be checked and none match assert list(map(type, RogTuring.probe(smbus, unsafe='smbus'))) == [] def test_rog_turing_assumes_device_if_unsafe_bus_unavailable(monkeypatch): smbus = create_strix_2080ti_oc_bus() # no addresses enabled # assume a device if the bus cannot be read due to missing unsafe features assert list(map(type, RogTuring.probe(smbus))) == [RogTuring] def test_rog_turing_finds_devices_on_any_addresses(monkeypatch): addresses = [0x29, 0x2a, 0x60] for addr in addresses: smbus = create_strix_2080ti_oc_bus(controller_address=addr) cards = list(RogTuring.probe(smbus, unsafe='smbus')) assert list(map(type, cards)) == [RogTuring] assert cards[0].address == hex(addr) def test_rog_turing_only_use_one_address(monkeypatch): smbus = create_strix_2080ti_oc_bus() # no addresses enabled addresses = [0x29, 0x2a, 0x60] smbus.open() for addr in addresses: smbus.write_byte_data(addr, 0x20, 0x15) smbus.write_byte_data(addr, 0x21, 0x89) smbus.close() cards = list(RogTuring.probe(smbus, unsafe='smbus')) assert list(map(type, cards)) == [RogTuring] assert cards[0].address == hex(addresses[0]) def test_rog_turing_unsafely_probed_is_not_usable(strix_2080ti_oc_bus): card = next(RogTuring.probe(strix_2080ti_oc_bus)) too_late = 'smbus' with pytest.raises(AssertionError): card.get_status(verbose=True, unsafe=too_late) with pytest.raises(AssertionError): card.set_color('led', 'off', [], unsafe=too_late) def test_rog_turing_get_status_is_noop(strix_2080ti_oc_bus): card = next(RogTuring.probe(strix_2080ti_oc_bus)) assert card.get_status() == [] def test_rog_turing_get_verbose_status_is_unsafe(strix_2080ti_oc_bus): card = next(RogTuring.probe(strix_2080ti_oc_bus)) assert card.get_status(verbose=True) == [] def test_rog_turing_gets_verbose_status(strix_2080ti_oc_bus): enable = ['smbus'] card = next(RogTuring.probe(strix_2080ti_oc_bus, unsafe=enable)) with card.connect(unsafe=enable): strix_2080ti_oc_bus.write_byte_data(0x2a, 0x07, 0x01) strix_2080ti_oc_bus.write_byte_data(0x2a, 0x04, 0xaa) strix_2080ti_oc_bus.write_byte_data(0x2a, 0x05, 0xbb) strix_2080ti_oc_bus.write_byte_data(0x2a, 0x06, 0xcc) status = card.get_status(verbose=True, unsafe=enable) expected = [ ('Mode', RogTuring.Mode.FIXED, ''), ('Color', 'aabbcc', ''), ] assert status == expected def test_rog_turing_set_color_is_unsafe(strix_2080ti_oc_bus): card = next(RogTuring.probe(strix_2080ti_oc_bus)) with pytest.raises(UnsafeFeaturesNotEnabled): assert card.set_color('led', 'off', []) def test_rog_turing_sets_color_to_off(strix_2080ti_oc_bus): enable = ['smbus'] card = next(RogTuring.probe(strix_2080ti_oc_bus, unsafe=enable)) with card.connect(unsafe=enable): # change colors to something other than 0 strix_2080ti_oc_bus.write_byte_data(0x2a, 0x04, 0xaa) strix_2080ti_oc_bus.write_byte_data(0x2a, 0x05, 0xbb) strix_2080ti_oc_bus.write_byte_data(0x2a, 0x06, 0xcc) card.set_color('led', 'off', [], unsafe=enable) assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x07) == 0x01 assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x04) == 0x00 assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x05) == 0x00 assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x06) == 0x00 # persistence not enabled assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x0e) == 0x00 def test_rog_turing_sets_color_to_fixed(strix_2080ti_oc_bus): enable = ['smbus'] card = next(RogTuring.probe(strix_2080ti_oc_bus, unsafe=enable)) with card.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] card.set_color('led', 'fixed', [radical_red], unsafe=enable) assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x07) == 0x01 assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x04) == 0xff assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x05) == 0x35 assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x06) == 0x5e # persistence not enabled assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x0e) == 0x00 def test_rog_turing_sets_color_to_rainbow(strix_2080ti_oc_bus): enable = ['smbus'] card = next(RogTuring.probe(strix_2080ti_oc_bus, unsafe=enable)) with card.connect(unsafe=enable): card.set_color('led', 'rainbow', [], unsafe=enable) assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x07) == 0x04 # persistence not enabled assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x0e) == 0x00 def test_rog_turing_sets_color_to_breathing(strix_2080ti_oc_bus): enable = ['smbus'] card = next(RogTuring.probe(strix_2080ti_oc_bus, unsafe=enable)) with card.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] card.set_color('led', 'breathing', [radical_red], unsafe=enable) assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x07) == 0x02 assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x04) == 0xff assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x05) == 0x35 assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x06) == 0x5e # persistence not enabled assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x0e) == 0x00 def test_rog_turing_sets_non_volatile_color(strix_2080ti_oc_bus): enable = ['smbus'] card = next(RogTuring.probe(strix_2080ti_oc_bus, unsafe=enable)) with card.connect(unsafe=enable): card.set_color('led', 'off', [], non_volatile=True, unsafe=enable) assert strix_2080ti_oc_bus.read_byte_data(0x2a, 0x0e) == 0x01 liquidctl-1.5.1/tests/test_nzxt_epsu.py000066400000000000000000000021101401367561700203260ustar00rootroot00000000000000import pytest from liquidctl.driver.nzxt_epsu import NzxtEPsu from _testutils import MockHidapiDevice, Report class _MockPsuDevice(MockHidapiDevice): def write(self, data): super().write(data) data = data[1:] # skip unused report ID reply = bytearray(64) reply[0:2] = (0xaa, data[2]) if data[5] == 0x06: reply[2] = data[2] - 2 elif data[5] == 0xfc: reply[2:4] = (0x11, 0x41) self.preload_read(Report(0, reply[0:])) @pytest.fixture def mockPsuDevice(): device = _MockPsuDevice() return NzxtEPsu(device, 'mock NZXT E500 PSU') def test_psu_device_initialize(mockPsuDevice): mockPsuDevice.initialize() assert len(mockPsuDevice.device.sent) == 0 def test_psu_device_status(mockPsuDevice): mockPsuDevice.connect() status = mockPsuDevice.get_status() fw = next(filter(lambda x: x[0] == 'Firmware version', status)) assert fw == ('Firmware version', 'A017/40983', '') sent = mockPsuDevice.device.sent assert sent[0] == Report(0, [0xad, 0, 3, 1, 0x60, 0xfc] + 58*[0]) liquidctl-1.5.1/tests/test_rgb_fusion2.py000066400000000000000000000175531401367561700205270ustar00rootroot00000000000000import pytest from collections import deque from liquidctl.driver.rgb_fusion2 import RgbFusion2 from _testutils import MockHidapiDevice, Report # Sample data for 5702 controller from a Gigabyte Z490 Vision D # https://github.com/liquidctl/liquidctl/issues/151#issuecomment-663213956 _INIT_5702_DATA = bytes.fromhex( 'cc01000701000a00000000004954353730322d47494741425954452056312e30' '2e31302e30000000000102000200010002000100000102000001025700000000' ) _INIT_5702_SAMPLE = Report(_INIT_5702_DATA[0], _INIT_5702_DATA[1:]) # Sample data for 8297 controller from a Gigabyte X570 Aorus Elite rev 1.0 # https://github.com/liquidctl/liquidctl/issues/151#issuecomment-663247422 # (note: original data had a trailing 0x61 byte, but that seems to be an artifact) _INIT_8297_DATA = bytes.fromhex( '00010001010006000000000049543832393742582d4742583537300000000000' '0000000000000000000000000200010002000100000102000001978200000000' ) _INIT_8297_SAMPLE = Report(_INIT_8297_DATA[0], _INIT_8297_DATA[1:]) @pytest.fixture def mockRgbFusion2_5702Device(): device = MockHidapiDevice(vendor_id=0x048d, product_id=0x5702, address='addr') dev = RgbFusion2(device, 'mock 5702 Controller') dev.connect() return dev class Mock8297HidInterface(MockHidapiDevice): def get_feature_report(self, report_id, length): """Get a feature report emulating out of spec behavior of the device.""" return super().get_feature_report(0, length) @pytest.fixture def mockRgbFusion2_8297Device(): device = Mock8297HidInterface(vendor_id=0x048d, product_id=0x8297, address='addr') dev = RgbFusion2(device, 'mock 8297 Controller') dev.connect() return dev def test_fusion2_5702_device_command_format(mockRgbFusion2_5702Device): mockRgbFusion2_5702Device.device.preload_read(_INIT_5702_SAMPLE) mockRgbFusion2_5702Device.initialize() mockRgbFusion2_5702Device.set_color(channel='sync', mode='off', colors=[]) assert len(mockRgbFusion2_5702Device.device.sent) == 1 + 8 + 1 for i, (report, data) in enumerate(mockRgbFusion2_5702Device.device.sent): assert report == 0xcc assert len(data) == 63 # TODO double check, more likely to be 64 def test_fusion2_5702_device_get_status(mockRgbFusion2_5702Device): assert mockRgbFusion2_5702Device.get_status() == [] def test_fusion2_5702_device_initialize_status(mockRgbFusion2_5702Device): mockRgbFusion2_5702Device.device.preload_read(_INIT_5702_SAMPLE) name, fw_version = mockRgbFusion2_5702Device.initialize() assert name[1] == "IT5702-GIGABYTE V1.0.10.0" assert fw_version[1] == '1.0.10.0' def test_fusion2_5702_device_off_with_some_channel(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80]] # should be ignored mockRgbFusion2_5702Device.set_color(channel='led8', mode='off', colors=iter(colors)) set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x27, 0x80] assert set_color.data[10] == 0x01 assert max(set_color.data[13:16]) == 0 assert max(set_color.data[21:27]) == 0 def test_fusion2_5702_device_fixed_with_some_channel(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockRgbFusion2_5702Device.set_color(channel='led7', mode='fixed', colors=iter(colors)) set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x26, 0x40] assert set_color.data[10] == 0x01 assert set_color.data[13:16] == [0x80, 0x00, 0xff] assert max(set_color.data[21:27]) == 0 def test_fusion2_5702_device_pulse_with_some_channel_and_speed(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockRgbFusion2_5702Device.set_color(channel='led3', mode='pulse', colors=iter(colors), speed='faster') set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x22, 0x04] assert set_color.data[10] == 0x02 assert set_color.data[13:16] == [0x80, 0x00, 0xff] assert set_color.data[21:27] == [0xe8, 0x03, 0xe8, 0x03, 0xf4, 0x01] def test_fusion2_5702_device_flash_with_some_channel_and_speed(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockRgbFusion2_5702Device.set_color(channel='led6', mode='flash', colors=iter(colors), speed='slowest') set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x25, 0x20] assert set_color.data[10] == 0x03 assert set_color.data[13:16] == [0x80, 0x00, 0xff] assert set_color.data[21:27] == [0x64, 0x00, 0x64, 0x00, 0x60, 0x09] def test_fusion2_5702_device_double_flash_with_some_channel_and_speed_and_uppercase(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockRgbFusion2_5702Device.set_color(channel='LED5', mode='DOUBLE-FLASH', colors=iter(colors), speed='LUDICROUS') set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x24, 0x10] assert set_color.data[10] == 0x03 assert set_color.data[13:16] == [0x80, 0x00, 0xff] assert set_color.data[21:27] == [0x64, 0x00, 0x64, 0x00, 0x40, 0x06] def test_fusion2_5702_device_color_cycle_with_some_channel_and_speed(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80]] # should be ignored mockRgbFusion2_5702Device.set_color(channel='led4', mode='color-cycle', colors=iter(colors), speed='fastest') set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x23, 0x08] assert set_color.data[10] == 0x04 assert max(set_color.data[13:16]) == 0 assert set_color.data[21:27] == [0x26, 0x02, 0xc2, 0x01, 0x00, 0x00] # TODO brightness def test_fusion2_5702_device_common_behavior_in_all_set_color_writes(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80]] for mode in ['off', 'fixed', 'pulse', 'flash', 'double-flash', 'color-cycle']: mockRgbFusion2_5702Device.device.sent = deque() mockRgbFusion2_5702Device.set_color(channel='led1', mode=mode, colors=iter(colors)) set_color, execute = mockRgbFusion2_5702Device.device.sent assert execute.data[0:2] == [0x28, 0xff] assert max(execute.data[2:]) == 0 def test_fusion2_5702_device_sync_channel(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80]] mockRgbFusion2_5702Device.set_color(channel='sync', mode='fixed', colors=iter(colors)) assert len(mockRgbFusion2_5702Device.device.sent) == 8 + 1 # 8 × set + execute def test_fusion2_5702_device_reset_all_channels(mockRgbFusion2_5702Device): mockRgbFusion2_5702Device.reset_all_channels() for addr, report in enumerate(mockRgbFusion2_5702Device.device.sent[:-1], 0x20): assert report.data[0:2] == [addr, 0] assert max(report.data[2:]) == 0 execute = mockRgbFusion2_5702Device.device.sent[-1] assert execute.data[0:2] == [0x28, 0xff] assert max(execute.data[2:]) == 0 def test_fusion2_5702_device_invalid_set_color_arguments(mockRgbFusion2_5702Device): with pytest.raises(KeyError): mockRgbFusion2_5702Device.set_color('invalid', 'off', []) with pytest.raises(KeyError): mockRgbFusion2_5702Device.set_color('led1', 'invalid', []) with pytest.raises(ValueError): mockRgbFusion2_5702Device.set_color('led1', 'fixed', []) with pytest.raises(KeyError): mockRgbFusion2_5702Device.set_color('led1', 'pulse', [[0xff, 0, 0x80]], speed='invalid') def test_fusion2_8297_device_initialize_status(mockRgbFusion2_8297Device): mockRgbFusion2_8297Device.device.preload_read(_INIT_8297_SAMPLE) name, fw_version = mockRgbFusion2_8297Device.initialize() assert name[1] == "IT8297BX-GBX570" assert fw_version[1] == '1.0.6.0' # other tests skipped, see Controller5702TestCase liquidctl-1.5.1/tests/test_smart_device.py000066400000000000000000000017631401367561700207510ustar00rootroot00000000000000import pytest from liquidctl.driver.smart_device import SmartDevice from _testutils import MockHidapiDevice, Report @pytest.fixture def mockSmartDevice(): device = MockHidapiDevice(vendor_id=0x1e71, product_id=0x1714, address='addr') return SmartDevice(device, 'mock NZXT Smart Device V1', speed_channel_count=3, color_channel_count=1) # class methods def test_smart_device_constructor(mockSmartDevice): assert mockSmartDevice._speed_channels == { 'fan1': (0, 0, 100), 'fan2': (1, 0, 100), 'fan3': (2, 0, 100), } assert mockSmartDevice._color_channels == {'led': (0), } def test_smart_device_not_totally_broken(mockSmartDevice): dev = mockSmartDevice for i in range(3): dev.device.preload_read(Report(0, bytes(63))) dev.initialize() dev.get_status() dev.set_color(channel='led', mode='breathing', colors=iter([[142, 24, 68]]), speed='fastest') dev.set_fixed_speed(channel='fan3', duty=50) liquidctl-1.5.1/tests/test_smart_device2.py000066400000000000000000000033731401367561700210320ustar00rootroot00000000000000import pytest from liquidctl.driver.smart_device import SmartDevice2 from _testutils import MockHidapiDevice, Report class _MockSmartDevice2(MockHidapiDevice): def __init__(self, raw_speed_channels, raw_led_channels): super().__init__() self.raw_speed_channels = raw_speed_channels self.raw_led_channels = raw_led_channels def write(self, data): reply = bytearray(64) if data[0:2] == [0x10, 0x01]: reply[0:2] = [0x11, 0x01] elif data[0:2] == [0x20, 0x03]: reply[0:2] = [0x21, 0x03] reply[14] = self.raw_led_channels if self.raw_led_channels > 1: reply[15 + 1 * 6] = 0x10 reply[15 + 2 * 6] = 0x11 self.preload_read(Report(reply[0], reply[1:])) @pytest.fixture def mockSmartDevice2(): device = _MockSmartDevice2(raw_speed_channels=3, raw_led_channels=2) dev = SmartDevice2(device, 'mock NZXT Smart Device V2', speed_channel_count=3, color_channel_count=2) dev.connect() return dev # class methods def test_smart_device2_constructor(mockSmartDevice2): assert mockSmartDevice2._speed_channels == { 'fan1': (0, 0, 100), 'fan2': (1, 0, 100), 'fan3': (2, 0, 100), } assert mockSmartDevice2._color_channels == { 'led1': (0b001), 'led2': (0b010), 'sync': (0b011), } def test_smart_device2_not_totally_broken(mockSmartDevice2): dev = mockSmartDevice2 dev.initialize() dev.device.preload_read(Report(0, [0x67, 0x02] + [0] * 62)) dev.get_status() dev.set_color(channel='led1', mode='breathing', colors=iter([[142, 24, 68]]), speed='fastest') dev.set_fixed_speed(channel='fan3', duty=50) liquidctl-1.5.1/tests/test_smbus.py000066400000000000000000000104611401367561700174300ustar00rootroot00000000000000from pathlib import Path from liquidctl.driver.smbus import LinuxI2c, LinuxI2cBus, SmbusDriver import pytest class Canary(SmbusDriver): """Canary driver to reveal if SMBus probing is taking place.""" @classmethod def probe(cls, smbus, **kwargs): yield Canary(smbus, 'Canary', vendor_id=-1, product_id=-1, address=-1) def connect(self, **kwargs): # there is no justification for calling connect on this test driver raise RuntimeError('forbidden') def __repr__(self): return repr(self._smbus) def replace_smbus(replacement, monkeypatch): import liquidctl.driver.smbus # fragile hack: since messing with sys.meta_path (PEP 302) is tricky in a # pytest context, get into liquidctl.driver.smbus module and replace the # imported SMBus class after the fact monkeypatch.setattr(liquidctl.driver.smbus, 'SMBus', replacement) return replacement @pytest.fixture def emulated_smbus(monkeypatch): """Replace the SMBus implementation in liquidctl.driver.smbus.""" class SMBus: def __init__(self, number): pass return replace_smbus(SMBus, monkeypatch) def test__helper_fixture_replaces_real_smbus_implementation(emulated_smbus, tmpdir): i2c_dev = Path(tmpdir.mkdir('i2c-9999')) # unlikely to be valid bus = LinuxI2cBus(i2c_dev=i2c_dev) bus.open() assert type(bus._smbus) == emulated_smbus def test_filter_by_usb_port_yields_no_devices(emulated_smbus): discovered = Canary.find_supported_devices(usb_port='usb1') assert discovered == [] def test_aborts_if_sysfs_is_missing_devices(emulated_smbus, tmpdir): empty = tmpdir.mkdir('sys').mkdir('bus').mkdir('i2c') virtual_bus = LinuxI2c(i2c_root=empty) discovered = Canary.find_supported_devices(root_bus=virtual_bus) assert discovered == [] def test_finds_a_device(emulated_smbus, tmpdir): i2c_root = tmpdir.mkdir('sys').mkdir('bus').mkdir('i2c') i2c_root.mkdir('devices').mkdir('i2c-42') virtual_bus = LinuxI2c(i2c_root=i2c_root) discovered = Canary.find_supported_devices(root_bus=virtual_bus) assert len(discovered) == 1 assert discovered[0]._smbus.name == 'i2c-42' def test_ignores_non_bus_sysfs_entries(emulated_smbus, tmpdir): i2c_root = tmpdir.mkdir('sys').mkdir('bus').mkdir('i2c') devices = i2c_root.mkdir('devices') devices.mkdir('i2c-0') devices.mkdir('0-0050') # SPD info chip on i2c-0 devices.mkdir('i2c-DELL0829:00') # i2c HID chip from Dell laptop virtual_bus = LinuxI2c(i2c_root=i2c_root) discovered = Canary.find_supported_devices(root_bus=virtual_bus) assert len(discovered) == 1 assert discovered[0]._smbus.name == 'i2c-0' def test_honors_a_bus_filter(emulated_smbus, tmpdir): i2c_root = tmpdir.mkdir('sys').mkdir('bus').mkdir('i2c') devices = i2c_root.mkdir('devices') devices.mkdir('i2c-0') devices.mkdir('i2c-1') virtual_bus = LinuxI2c(i2c_root=i2c_root) discovered = Canary.find_supported_devices(bus='i2c-1', root_bus=virtual_bus) assert len(discovered) == 1 assert discovered[0]._smbus.name == 'i2c-1' @pytest.fixture def emulated_device(tmpdir, emulated_smbus): i2c_dev = Path(tmpdir.mkdir('i2c-0')) bus = LinuxI2cBus(i2c_dev=i2c_dev) dev = SmbusDriver(smbus=bus, description='Test', vendor_id=-1, product_id=-1, address=-1) return (bus, dev) def test_connect_is_unsafe(emulated_device): bus, dev = emulated_device def mock_open(): nonlocal opened opened = True bus.open = mock_open opened = False dev.connect() assert not opened def test_connects(emulated_device): bus, dev = emulated_device def mock_open(): nonlocal opened opened = True bus.open = mock_open opened = False with dev.connect(unsafe='smbus') as cm: assert cm == dev assert opened def test_loading_unnavailable_eeprom_returns_none(emulated_device): bus, dev = emulated_device assert bus.load_eeprom(0x51) is None def test_loads_eeprom(emulated_device): bus, dev = emulated_device spd = bus._i2c_dev.joinpath('0-0051') spd.mkdir() spd.joinpath('eeprom').write_bytes(b'012345') spd.joinpath('name').write_text('name\n') assert bus.load_eeprom(0x51) == ('name', b'012345') liquidctl-1.5.1/tests/test_usb.py000066400000000000000000000026261401367561700170740ustar00rootroot00000000000000import pytest from liquidctl.driver.usb import UsbDriver, UsbHidDriver from _testutils import MockHidapiDevice @pytest.fixture def emulated_hid_device(): hiddev = MockHidapiDevice() return UsbHidDriver(hiddev, 'Test') @pytest.fixture def emulated_usb_device(): usbdev = MockHidapiDevice() # hack, should mock PyUsbDevice dev = UsbDriver(usbdev, 'Test') return dev def test_hid_connects(emulated_hid_device): dev = emulated_hid_device def mock_open(): nonlocal opened opened = True dev.device.open = mock_open opened = False with dev.connect() as cm: assert cm == dev assert opened def test_hid_disconnect(emulated_hid_device): dev = emulated_hid_device def mock_close(): nonlocal opened opened = False dev.device.close = mock_close opened = True dev.disconnect() assert not opened def test_usb_connects(emulated_usb_device): dev = emulated_usb_device def mock_open(): nonlocal opened opened = True dev.device.open = mock_open opened = False with dev.connect() as cm: assert cm == dev assert opened def test_usb_disconnect(emulated_usb_device): dev = emulated_usb_device def mock_close(): nonlocal opened opened = False dev.device.close = mock_close opened = True dev.disconnect() assert not opened