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