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