Add fan speed
[khatus.git] / bin / khatus_loop
1 #! /bin/bash
2
3 set -e
4
5 produce_fan() {
6 fan_path="$1"
7 cat "$fan_path"
8 }
9
10 produce_temperature() {
11 thermal_zone="$1"
12 cat "/sys/class/thermal/thermal_zone${thermal_zone}/temp"
13 }
14
15 produce_loadavg() {
16 cat /proc/loadavg
17 }
18
19 produce_disk_io() {
20 disk_io_device="$1"
21 awk '
22 {
23 r = $3
24 w = $7
25 print w, r
26 }
27 ' "/sys/block/$disk_io_device/stat"
28 }
29
30 produce_disk_space() {
31 disk_space_device="$1"
32 df --output=pcent "$disk_space_device" | awk 'NR == 2 {print $1}'
33 }
34
35 produce_net_addr_io() {
36 ip -s addr \
37 | awk '
38 BEGIN {
39 bytes_per_unit = 1024 * 1024
40 }
41
42 /^[0-9]+:/ {
43 sub(":$", "", $1)
44 sub(":$", "", $2)
45 sequence = $1
46 interface = $2
47 interfaces[sequence] = interface
48 }
49
50 /^ +inet [0-9]/ {
51 sub("/[0-9]+", "", $2)
52 addr = $2
53 addrs[interface] = addr
54 }
55
56 /^ +RX: / {transfer_direction = "r"}
57 /^ +TX: / {transfer_direction = "w"}
58
59 /^ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ *$/ {
60 io[interface, transfer_direction] = $1;
61 }
62
63 END {
64 for (seq=1; seq<=sequence; seq++) {
65 interface = interfaces[seq]
66 label = substr(interface, 1, 1)
67 if (addrs[interface]) {
68 curr_read = io[interface, "r"]
69 curr_write = io[interface, "w"]
70 print(interface, addrs[interface], curr_write, curr_read)
71 } else {
72 print(interface)
73 }
74 }
75 }'
76 }
77
78 produce_net_wifi_status() {
79 nmcli \
80 -f ACTIVE,SSID,SIGNAL \
81 -t \
82 d wifi \
83 | awk \
84 -F ':' \
85 '
86 BEGIN {wifi_status = "--"}
87 $1 == "yes" {wifi_status = $2 ":" $3 "%"}
88 END {print wifi_status}
89 '
90 }
91
92 produce_bluetooth_power() {
93 echo -e 'show \n quit' \
94 | bluetoothctl \
95 | awk '
96 /^Controller / {
97 controller = $2;
98 controllers[++ctrl_count] = controller;
99 }
100 /^\t[A-Z][A-Za-z]+:/ {
101 key = $1;
102 sub(":$", "", key);
103 val = $2;
104 for (i=3; i<=NF; i++) {
105 val = val " " $i};
106 data[controller, key] = val;
107 }
108 END {
109 # Using the 1st seen controller. Should we select specific instead?
110 power_status = data[controllers[1], "Powered"];
111 if (ctrl_count > 0) {
112 if (power_status == "no") {
113 power_status = "off"
114 } else if (power_status == "yes") {
115 power_status = "on"
116 } else {
117 printf("Unexpected bluetooth power status: %s\n", power_status)\
118 > "/dev/stderr";
119 power_status = "ERROR"
120 }
121 } else {
122 power_status = "off" # TODO: Perhaps use differentiated marker?
123 }
124 printf("%s\n", power_status);
125 }'
126 }
127
128 produce_screen_brightness() {
129 screen_brightness_device_path="$1"
130 echo "\
131 $(cat $screen_brightness_device_path/max_brightness) \
132 $(cat $screen_brightness_device_path/brightness)\
133 "
134 }
135
136 produce_volume() {
137 pactl list sinks \
138 | awk '
139 /^\tMute:/ {
140 printf("%s,", $0);
141 }
142 /^\tVolume:/ {
143 for (i=2; i<=NF; i++) printf(" %s", $i);
144 }' \
145 | awk -v RS=',' '
146 /^[ \t]*Mute:/ {mute = $2}
147 /^[ \t]*front-left:/ {left = $4}
148 /^[ \t]*front-right:/ {right = $4}
149 END {
150 if (mute == "yes") {
151 print("x")
152 } else {
153 print("%s %s\n", left, right)
154 }
155 }
156 '
157 }
158
159 produce_mpd_state() {
160 echo 'status' \
161 | nc 127.0.0.1 6600 \
162 | awk '
163 {
164 status[$1] = $2
165 }
166
167 /^time: +[0-9]+:[0-9]+$/ {
168 split($2, time, ":")
169 seconds_current = time[1]
170 seconds_total = time[2]
171
172 hours = int(seconds_current / 60 / 60);
173 secs_beyond_hours = seconds_current - (hours * 60 * 60);
174 mins = int(secs_beyond_hours / 60);
175 secs = secs_beyond_hours - (mins * 60);
176 if (hours > 0) {
177 current_time = sprintf("%d:%.2d:%.2d", hours, mins, secs)
178 } else {
179 current_time = sprintf("%.2d:%.2d", mins, secs)
180 }
181
182 if (seconds_total > 0) {
183 time_percentage = (seconds_current / seconds_total) * 100
184 current_percentage = sprintf("%d%%", time_percentage)
185 } else {
186 current_percentage = "~"
187 }
188 }
189
190 END {
191 state = status["state:"]
192
193 if (state == "play") {
194 symbol = "▶"
195 } else if (state == "pause") {
196 symbol = "❚❚"
197 } else if (state == "stop") {
198 symbol = "⬛"
199 } else {
200 symbol = "--"
201 }
202
203 printf(\
204 "%s %s %s\n",
205 status["state:"], current_time, current_percentage\
206 )
207 }
208 '
209 }
210
211 produce_mpd_song() {
212 echo 'currentsong' \
213 | nc 127.0.0.1 6600 \
214 | awk '
215 /^OK/ {
216 next
217 }
218
219 {
220 key = $1
221 sub("^" key " +", "")
222 val = $0
223 data[key] = val
224 }
225
226 END {
227 name = data["Name:"]
228 title = data["Title:"]
229 file = data["file:"]
230
231 if (name) {
232 out = name
233 } else if (title) {
234 out = title
235 } else if (file) {
236 last = split(file, parts, "/")
237 out = parts[last]
238 } else {
239 out = ""
240 }
241 print out
242 }
243 '
244 }
245
246 produce_weather() {
247 weather_station_id="$1"
248 metar -d "$weather_station_id" 2>&1 \
249 | awk '
250 /METAR pattern not found in NOAA data/ {
251 failures++
252 }
253
254 /^Temperature/ {
255 celsius = $3;
256 fahrenheit = (celsius * (9 / 5)) + 32;
257 temperature = fahrenheit
258 }
259
260 END {
261 if (failures > 0) {
262 temperature = "--"
263 }
264 print temperature "°F"
265 }'
266 }
267
268 produce_datetime() {
269 date +'%a %b %d %H:%M:%S'
270 }
271
272 consume() {
273 pipe="$1"
274 debug="$2"
275 prefixes_of_net_interfaces_to_show="$3"
276 tail -f "$pipe" \
277 | stdbuf -o L awk \
278 -v opt_debug="$debug" \
279 -v opt_mpd_song_max_chars=10 \
280 -v opt_prefixes_of_net_interfaces_to_show="$prefixes_of_net_interfaces_to_show" \
281 '
282 /^in:FAN +status:/\
283 {
284 split_msg_parts()
285 db["fan_status"] = $2
286 }
287
288 /^in:FAN +speed:/\
289 {
290 split_msg_parts()
291 db["fan_speed"] = $2
292 }
293
294 /^in:FAN +level:/\
295 {
296 split_msg_parts()
297 db["fan_level"] = $2
298 }
299
300 /^in:TEMPERATURE/\
301 {
302 split_msg_parts()
303 db["temperature"] = $1
304 }
305
306 /^in:LOAD_AVG/\
307 {
308 split_msg_parts()
309 set_load_avg()
310 }
311
312 /^in:DISK_IO/\
313 {
314 split_msg_parts()
315 set_disk_io()
316 }
317
318 /^in:DISK_SPACE/\
319 {
320 split_msg_parts()
321 db["disk_space_used"] = msg_body
322 }
323
324 /^in:NET_ADDR_IO/\
325 {
326 split_msg_parts()
327 set_net_addr_io()
328 }
329
330 /^in:NET_WIFI_STATUS/\
331 {
332 split_msg_parts()
333 db["net_wifi_status"] = msg_body
334 }
335
336 /^in:BLUETOOTH_POWER/\
337 {
338 split_msg_parts()
339 db["bluetooth_power"] = msg_body
340 }
341
342 /^in:SCREEN_BRIGHTNESS/\
343 {
344 split_msg_parts()
345 set_screen_brightness()
346 }
347
348 /^in:VOLUME/\
349 {
350 split_msg_parts()
351 db["volume"] = msg_body
352 }
353
354 /^in:MPD_STATE/\
355 {
356 split_msg_parts()
357 db["mpd_state"] = $1
358 db["mpd_curr_song_time"] = $2
359 db["mpd_curr_song_percent"] = $3
360 }
361
362 /^in:MPD_SONG/\
363 {
364 split_msg_parts()
365 db["mpd_curr_song_name"] = msg_body
366 }
367
368 /^in:WEATHER/\
369 {
370 split_msg_parts()
371 db["weather_temperature"] = msg_body
372 }
373
374 /^in:DATE_TIME/\
375 {
376 split_msg_parts()
377 db["datetime"] = msg_body
378 }
379
380 /^out:BAR/\
381 {
382 split_msg_parts()
383 print make_bar()
384 }
385
386 function set_load_avg( sched) {
387 split($4, sched, "/")
388 db["load_avg_1min"] = $1
389 db["load_avg_5min"] = $2
390 db["load_avg_15min"] = $3
391 db["kern_sched_queue_runnable"] = sched[1]
392 db["kern_sched_queue_total"] = sched[2]
393 db["kern_sched_latest_pid"] = $5
394 }
395
396 function set_disk_io( curr_w, curr_r, prev_w, prev_r) {
397 curr_w = $1
398 curr_r = $2
399 prev_w = db["disk_io_curr_w"]
400 prev_r = db["disk_io_curr_r"]
401 db["disk_io_curr_w"] = curr_w
402 db["disk_io_curr_r"] = curr_r
403 db["disk_io_diff_w"] = curr_w - prev_w
404 db["disk_io_diff_r"] = curr_r - prev_r
405 }
406
407 function set_net_addr_io( \
408 interface, address, io_curr_w, io_curr_r, io_prev_w, io_prev_r\
409 ) {
410 interface = $1
411 address = $2
412 io_curr_w = $3
413 io_curr_r = $4
414 if (interface) {
415 if (address && io_curr_w && io_curr_r) {
416 # recalculate
417 io_prev_w = net_io_curr_w[interface]
418 io_prev_r = net_io_curr_r[interface]
419
420 net_addr[interface] = address
421 net_io_curr_w[interface] = io_curr_w
422 net_io_curr_r[interface] = io_curr_r
423 net_io_diff_w[interface] = io_curr_w - io_prev_w
424 net_io_diff_r[interface] = io_curr_r - io_prev_r
425 } else {
426 # clear
427 net_addr[interface] = ""
428 net_io_curr_w[interface] = 0
429 net_io_curr_r[interface] = 0
430 net_io_diff_w[interface] = 0
431 net_io_diff_r[interface] = 0
432 }
433 }
434 }
435
436 function set_screen_brightness( max, cur) {
437 max = $1
438 cur = $2
439 db["screen_brightness"] = (cur / max) * 100
440 }
441
442 function split_msg_parts() {
443 msg_head = $1
444 sub("^" msg_head " +", "")
445 msg_body = $0
446 debug(msg_head, msg_body)
447 }
448
449 function make_bar( position, bar, sep, i, j) {
450 position[++i] = make_status_cpu()
451 position[++i] = make_status_disk()
452 position[++i] = make_status_net()
453 position[++i] = sprintf("B=%s", db["bluetooth_power"])
454 position[++i] = sprintf("*%d%%", db["screen_brightness"])
455 position[++i] = sprintf("(%s)", db["volume"])
456 position[++i] = make_status_mpd()
457 position[++i] = db["weather_temperature"]
458 position[++i] = db["datetime"]
459 bar = ""
460 sep = ""
461 for (j = 1; j <= i; j++) {
462 bar = bar sep position[j]
463 sep = " "
464 }
465 return bar
466 }
467
468 function make_status_cpu( load, temp, fan) {
469 load = db["load_avg_1min"]
470 temp = db["temperature"] / 1000
471 fan = db["fan_speed"]
472 return sprintf("C=[%4.2f %d°C %4drpm]", load, temp, fan)
473 }
474
475 function make_status_disk( bytes_per_sector, bytes_per_mb, w, r) {
476 bytes_per_sector = 512
477 bytes_per_mb = 1024 * 1024
478 w = (db["disk_io_diff_w"] * bytes_per_sector) / bytes_per_mb
479 r = (db["disk_io_diff_r"] * bytes_per_sector) / bytes_per_mb
480 return \
481 sprintf("D=[%s %0.3f▲ %0.3f▼]", db["disk_space_used"], w, r)
482 }
483
484 function make_status_net( \
485 out,
486 number_of_interfaces_to_show,
487 n,
488 array_of_prefixes_of_interfaces_to_show,
489 prefix,
490 interface,
491 label,
492 count_printed,
493 sep,
494 io_stat,
495 dw, dr,
496 bytes_per_unit\
497 ) {
498 out = ""
499 number_of_interfaces_to_show = \
500 split(\
501 opt_prefixes_of_net_interfaces_to_show,\
502 array_of_prefixes_of_interfaces_to_show,\
503 ","\
504 )
505 for (n = 1; n <= number_of_interfaces_to_show; n++) {
506 prefix = array_of_prefixes_of_interfaces_to_show[n]
507 for (interface in net_addr) {
508 if (interface ~ ("^" prefix)) {
509 label = substr(interface, 1, 1)
510 if (net_addr[interface]) {
511 bytes_per_mb = 1024 * 1024 # TODO: option
512 dw = net_io_diff_w[interface] / bytes_per_mb
513 dr = net_io_diff_r[interface] / bytes_per_mb
514 io_stat = sprintf("%0.3f▲ %0.3f▼", dw, dr)
515 } else {
516 io_stat = "--"
517 }
518 if (interface ~ "^w") {
519 label = label ":" db["net_wifi_status"]
520 }
521 if (++count_printed > 1) {
522 sep = " "
523 } else {
524 sep = ""
525 }
526 out = out sep label ":" io_stat
527 }
528 }
529 }
530 return sprintf("N[%s]", out)
531 }
532
533 function make_status_mpd( state, status) {
534 state = db["mpd_state"]
535
536 if (state == "play") {
537 status = make_status_mpd_state_known("▶")
538 } else if (state == "pause") {
539 status = make_status_mpd_state_known("❚❚")
540 } else if (state == "stop") {
541 status = make_status_mpd_state_known("⬛")
542 } else {
543 status = make_status_mpd_state_unknown("--")
544 }
545
546 return sprintf("[%s]", status)
547 }
548
549 function make_status_mpd_state_known(symbol) {
550 return sprintf(\
551 "%s %s %s %s",
552 symbol,
553 db["mpd_curr_song_time"],
554 db["mpd_curr_song_percent"],
555 substr(db["mpd_curr_song_name"], 1, opt_mpd_song_max_chars)\
556 )
557 }
558
559 function make_status_mpd_state_unknown(symbol) {
560 return sprintf("%s", symbol)
561 }
562
563 function debug(location, msg) {
564 if (opt_debug) {
565 print_error(location, msg)
566 }
567 }
568
569 function print_error(location, msg) {
570 print(location " ==> " msg) > "/dev/stderr"
571 }
572 '
573 }
574
575 produce_bar_req() {
576 echo ''
577 }
578
579 spawn() {
580 cmd="$1"
581 pipe="$2"
582 msg_head="$3"
583 interval="$4"
584 while true; do
585 $cmd | while read line; do
586 echo "${msg_head} $line" > "$pipe"
587 done
588 sleep "$interval"
589 done &
590 }
591
592 main() {
593 # Defaults
594 debug=0
595 dir_data="$HOME/.khatus"
596 weather_station_id='KJFK'
597 screen_brightness_device_name='acpi_video0'
598 prefixes_of_net_interfaces_to_show='w' # comma-separated
599 disk_space_device='/'
600 disk_io_device='sda'
601 thermal_zone=0
602 fan_path='/proc/acpi/ibm/fan'
603
604 # User-overrides
605 long_options=''
606 long_options+='debug'
607 long_options+=',data-dir:'
608 long_options+=',weather-station:'
609 long_options+=',screen-device:'
610 long_options+=',prefixes_of_net_interfaces_to_show:'
611 long_options+=',disk_space_device:'
612 long_options+=',thermal_zone:'
613 long_options+=',fan_path:'
614 OPTS=$(
615 getopt \
616 -o 'd' \
617 -l $long_options \
618 -- "$@"
619 )
620 eval set -- "$OPTS"
621 while true
622 do
623 case "$1" in
624 -d|--debug)
625 debug=1
626 shift
627 ;;
628 --data-dir)
629 dir_data="$2"
630 shift 2
631 ;;
632 --weather-station)
633 weather_station_id="$2"
634 shift 2
635 ;;
636 --screen-device)
637 screen_brightness_device_name="$2"
638 shift 2
639 ;;
640 --prefixes_of_net_interfaces_to_show)
641 prefixes_of_net_interfaces_to_show="$2"
642 shift 2
643 ;;
644 --disk_space_device)
645 disk_space_device="$2"
646 shift 2
647 ;;
648 --disk_io_device)
649 disk_io_device="$2"
650 shift 2
651 ;;
652 --thermal_zone)
653 thermal_zone="$2"
654 shift 2
655 ;;
656 --fan_path)
657 fan_path="$2"
658 shift 2
659 ;;
660 --)
661 shift
662 break
663 ;;
664 esac
665 done
666
667 pipe="$dir_data/khatus_data_pipe"
668 screen_brightness_device_path='/sys/class/backlight'
669 screen_brightness_device_path+="/$screen_brightness_device_name"
670
671 ( echo "Khatus starting with the following parameters:"
672 ( echo " debug|= $debug"
673 echo " dir_data|= $dir_data"
674 echo " pipe|= $pipe"
675 echo " screen_brightness_device_name|= $screen_brightness_device_name"
676 echo " screen_brightness_device_path|= $screen_brightness_device_path"
677 echo " weather_station_id|= $weather_station_id"
678 echo " prefixes_of_net_interfaces_to_show|= $prefixes_of_net_interfaces_to_show"
679 echo " disk_space_device|= $disk_space_device"
680 echo " disk_io_device|= $disk_io_device"
681 echo " thermal_zone|= $thermal_zone"
682 echo " fan_path|= $fan_path"
683 ) | column -ts\|
684 echo ''
685 ) >&2
686
687 mkdir -p "$dir_data"
688 rm -f "$pipe"
689 mkfifo "$pipe"
690
691 cmd_produce_screen_brightness='produce_screen_brightness'
692 cmd_produce_screen_brightness+=" $screen_brightness_device_path"
693
694 cmd_produce_weather="produce_weather $weather_station_id"
695
696 cmd_produce_disk_space="produce_disk_space $disk_space_device"
697
698 cmd_produce_disk_io="produce_disk_io $disk_io_device"
699
700 cmd_produce_temperature="produce_temperature $thermal_zone"
701
702 cmd_produce_fan="produce_fan $fan_path"
703
704 # TODO: Redirect each worker's stderr to a dedicated log file
705 spawn produce_datetime "$pipe" 'in:DATE_TIME' 1
706 spawn "$cmd_produce_screen_brightness" "$pipe" 'in:SCREEN_BRIGHTNESS' 1
707 spawn "$cmd_produce_weather" "$pipe" 'in:WEATHER' $(( 30 * 60 ))
708 spawn produce_mpd_state "$pipe" 'in:MPD_STATE' 1
709 spawn produce_mpd_song "$pipe" 'in:MPD_SONG' 1
710 spawn produce_volume "$pipe" 'in:VOLUME' 1
711 spawn produce_bluetooth_power "$pipe" 'in:BLUETOOTH_POWER' 5
712 spawn produce_net_wifi_status "$pipe" 'in:NET_WIFI_STATUS' 5
713 spawn produce_net_addr_io "$pipe" 'in:NET_ADDR_IO' 1
714 spawn "$cmd_produce_disk_space" "$pipe" 'in:DISK_SPACE' 1
715 spawn "$cmd_produce_disk_io" "$pipe" 'in:DISK_IO' 1
716 spawn produce_loadavg "$pipe" 'in:LOAD_AVG' 1
717 spawn "$cmd_produce_temperature" "$pipe" 'in:TEMPERATURE' 1
718 spawn "$cmd_produce_fan" "$pipe" 'in:FAN' 1
719 spawn produce_bar_req "$pipe" 'out:BAR' 1
720
721 consume \
722 "$pipe" \
723 "$debug" \
724 "$prefixes_of_net_interfaces_to_show"
725 }
726
727 main $@
This page took 0.227857 seconds and 4 git commands to generate.