1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.services.thinkfan;
10 settingsFormat = pkgs.formats.yaml { };
11 configFile = settingsFormat.generate "thinkfan.yaml" cfg.settings;
12 thinkfan = pkgs.thinkfan.override { inherit (cfg) smartSupport; };
13
14 # fan-speed and temperature levels
15 levelType =
16 with lib.types;
17 let
18 tuple =
19 ts:
20 lib.mkOptionType {
21 name = "tuple";
22 merge = lib.mergeOneOption;
23 check = xs: lib.all lib.id (lib.zipListsWith (t: x: t.check x) ts xs);
24 description = "tuple of" + lib.concatMapStrings (t: " (${t.description})") ts;
25 };
26 level = ints.unsigned;
27 special = enum [
28 "level auto"
29 "level full-speed"
30 "level disengaged"
31 ];
32 in
33 tuple [
34 (either level special)
35 level
36 level
37 ];
38
39 # sensor or fan config
40 sensorType =
41 name:
42 lib.types.submodule {
43 freeformType = lib.types.attrsOf settingsFormat.type;
44 options =
45 {
46 type = lib.mkOption {
47 type = lib.types.enum [
48 "hwmon"
49 "atasmart"
50 "tpacpi"
51 "nvml"
52 ];
53 description = ''
54 The ${name} type, can be
55 `hwmon` for standard ${name}s,
56
57 `atasmart` to read the temperature via
58 S.M.A.R.T (requires smartSupport to be enabled),
59
60 `tpacpi` for the legacy thinkpac_acpi driver, or
61
62 `nvml` for the (proprietary) nVidia driver.
63 '';
64 };
65 query = lib.mkOption {
66 type = lib.types.str;
67 description = ''
68 The query string used to match one or more ${name}s: can be
69 a fullpath to the temperature file (single ${name}) or a fullpath
70 to a driver directory (multiple ${name}s).
71
72 ::: {.note}
73 When multiple ${name}s match, the query can be restricted using the
74 {option}`name` or {option}`indices` options.
75 :::
76 '';
77 };
78 indices = lib.mkOption {
79 type = with lib.types; nullOr (listOf ints.unsigned);
80 default = null;
81 description = ''
82 A list of ${name}s to pick in case multiple ${name}s match the query.
83
84 ::: {.note}
85 Indices start from 0.
86 :::
87 '';
88 };
89 }
90 // lib.optionalAttrs (name == "sensor") {
91 correction = lib.mkOption {
92 type = with lib.types; nullOr (listOf int);
93 default = null;
94 description = ''
95 A list of values to be added to the temperature of each sensor,
96 can be used to equalize small discrepancies in temperature ratings.
97 '';
98 };
99 };
100 };
101
102 # removes NixOS special and unused attributes
103 sensorToConf =
104 { type, query, ... }@args:
105 (lib.filterAttrs (
106 k: v:
107 v != null
108 && !(lib.elem k [
109 "type"
110 "query"
111 ])
112 ) args)
113 // {
114 "${type}" = query;
115 };
116
117 syntaxNote = name: ''
118 ::: {.note}
119 This section slightly departs from the thinkfan.conf syntax.
120 The type and path must be specified like this:
121 ```
122 type = "tpacpi";
123 query = "/proc/acpi/ibm/${name}";
124 ```
125 instead of a single declaration like:
126 ```
127 - tpacpi: /proc/acpi/ibm/${name}
128 ```
129 :::
130 '';
131
132in
133{
134
135 options = {
136
137 services.thinkfan = {
138
139 enable = lib.mkOption {
140 type = lib.types.bool;
141 default = false;
142 description = ''
143 Whether to enable thinkfan, a fan control program.
144
145 ::: {.note}
146 This module targets IBM/Lenovo thinkpads by default, for
147 other hardware you will have configure it more carefully.
148 :::
149 '';
150 relatedPackages = [ "thinkfan" ];
151 };
152
153 smartSupport = lib.mkOption {
154 type = lib.types.bool;
155 default = false;
156 description = ''
157 Whether to build thinkfan with S.M.A.R.T. support to read temperatures
158 directly from hard disks.
159 '';
160 };
161
162 sensors = lib.mkOption {
163 type = lib.types.listOf (sensorType "sensor");
164 default = [
165 {
166 type = "tpacpi";
167 query = "/proc/acpi/ibm/thermal";
168 }
169 ];
170 description = ''
171 List of temperature sensors thinkfan will monitor.
172
173 ${syntaxNote "thermal"}
174 '';
175 };
176
177 fans = lib.mkOption {
178 type = lib.types.listOf (sensorType "fan");
179 default = [
180 {
181 type = "tpacpi";
182 query = "/proc/acpi/ibm/fan";
183 }
184 ];
185 description = ''
186 List of fans thinkfan will control.
187
188 ${syntaxNote "fan"}
189 '';
190 };
191
192 levels = lib.mkOption {
193 type = lib.types.listOf levelType;
194 default = [
195 [
196 0
197 0
198 55
199 ]
200 [
201 1
202 48
203 60
204 ]
205 [
206 2
207 50
208 61
209 ]
210 [
211 3
212 52
213 63
214 ]
215 [
216 6
217 56
218 65
219 ]
220 [
221 7
222 60
223 85
224 ]
225 [
226 "level auto"
227 80
228 32767
229 ]
230 ];
231 description = ''
232 [LEVEL LOW HIGH]
233
234 LEVEL is the fan level to use: it can be an integer (0-7 with thinkpad_acpi),
235 "level auto" (to keep the default firmware behavior), "level full-speed" or
236 "level disengaged" (to run the fan as fast as possible).
237 LOW is the temperature at which to step down to the previous level.
238 HIGH is the temperature at which to step up to the next level.
239 All numbers are integers.
240 '';
241 };
242
243 extraArgs = lib.mkOption {
244 type = lib.types.listOf lib.types.str;
245 default = [ ];
246 example = [
247 "-b"
248 "0"
249 ];
250 description = ''
251 A list of extra command line arguments to pass to thinkfan.
252 Check the {manpage}`thinkfan(1)` manpage for available arguments.
253 '';
254 };
255
256 settings = lib.mkOption {
257 type = lib.types.attrsOf settingsFormat.type;
258 default = { };
259 description = ''
260 Thinkfan settings. Use this option to configure thinkfan
261 settings not exposed in a NixOS option or to bypass one.
262 Before changing this, read the {manpage}`thinkfan.conf(5)`
263 manpage and take a look at the example config file at
264 <https://github.com/vmatare/thinkfan/blob/master/examples/thinkfan.yaml>
265 '';
266 };
267
268 };
269
270 };
271
272 config = lib.mkIf cfg.enable {
273
274 environment.systemPackages = [ thinkfan ];
275
276 services.thinkfan.settings = lib.mapAttrs (k: v: lib.mkDefault v) {
277 sensors = map sensorToConf cfg.sensors;
278 fans = map sensorToConf cfg.fans;
279 levels = cfg.levels;
280 };
281
282 systemd.packages = [ thinkfan ];
283
284 systemd.services = {
285 thinkfan.environment.THINKFAN_ARGS = lib.escapeShellArgs (
286 [
287 "-c"
288 configFile
289 ]
290 ++ cfg.extraArgs
291 );
292 thinkfan.serviceConfig = {
293 Restart = "on-failure";
294 RestartSec = "30s";
295
296 # Hardening
297 PrivateNetwork = true;
298 };
299
300 # must be added manually, see issue #81138
301 thinkfan.wantedBy = [ "multi-user.target" ];
302 thinkfan-wakeup.wantedBy = [ "sleep.target" ];
303 thinkfan-sleep.wantedBy = [ "sleep.target" ];
304 };
305
306 boot.extraModprobeConfig = "options thinkpad_acpi experimental=1 fan_control=1";
307
308 };
309}