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