Add energy
[khatus.git] / bin / khatus_loop
index 3bd112d..edefb98 100755 (executable)
@@ -1,9 +1,267 @@
 #! /bin/bash
 
-MSG_TAG_SEP=': '
+set -e
 
-fetch_weather() {
-    metar -d "$WEATHER_STATION_ID" 2>&1 \
+produce_energy() {
+    upower -e \
+    | grep battery \
+    | xargs upower -i \
+    | awk '
+        /^ +percentage: +/ { percentage=$2 }
+        /^ +state: +/      { state=$2 }
+        END                { print(state, percentage) }
+        '
+}
+
+
+produce_memory() {
+    free | awk '$1 == "Mem:" {print $2, $3}'
+}
+
+produce_fan() {
+    fan_path="$1"
+    cat "$fan_path"
+}
+
+produce_temperature() {
+    thermal_zone="$1"
+    cat "/sys/class/thermal/thermal_zone${thermal_zone}/temp"
+}
+
+produce_loadavg() {
+    cat /proc/loadavg
+}
+
+produce_disk_io() {
+    disk_io_device="$1"
+    awk '
+        {
+            r = $3
+            w = $7
+            print w, r
+        }
+        ' "/sys/block/$disk_io_device/stat"
+}
+
+produce_disk_space() {
+    disk_space_device="$1"
+    df --output=pcent "$disk_space_device" | awk 'NR == 2 {print $1}'
+}
+
+produce_net_addr_io() {
+    ip -s addr \
+    | awk '
+        BEGIN {
+            bytes_per_unit = 1024 * 1024
+        }
+
+        /^[0-9]+:/ {
+            sub(":$", "", $1)
+            sub(":$", "", $2)
+            sequence = $1
+            interface = $2
+            interfaces[sequence] = interface
+        }
+
+        /^ +inet [0-9]/ {
+            sub("/[0-9]+", "", $2)
+            addr = $2
+            addrs[interface] = addr
+        }
+
+        /^ +RX: / {transfer_direction = "r"}
+        /^ +TX: / {transfer_direction = "w"}
+
+        /^ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ +[0-9]+ *$/ {
+            io[interface, transfer_direction] = $1;
+        }
+
+        END {
+            for (seq=1; seq<=sequence; seq++) {
+                interface = interfaces[seq]
+                label = substr(interface, 1, 1)
+                if (addrs[interface]) {
+                    curr_read  = io[interface, "r"]
+                    curr_write = io[interface, "w"]
+                    print(interface, addrs[interface], curr_write, curr_read)
+                } else {
+                    print(interface)
+                }
+            }
+        }'
+}
+
+produce_net_wifi_status() {
+    nmcli \
+        -f ACTIVE,SSID,SIGNAL \
+        -t \
+        d wifi \
+    | awk \
+        -F ':' \
+        '
+        BEGIN       {wifi_status = "--"}
+        $1 == "yes" {wifi_status =  $2 ":" $3 "%"}
+        END         {print wifi_status}
+        '
+}
+
+produce_bluetooth_power() {
+    echo -e 'show \n quit' \
+    | bluetoothctl \
+    | awk '
+        /^Controller / {
+            controller = $2;
+            controllers[++ctrl_count] = controller;
+        }
+        /^\t[A-Z][A-Za-z]+:/ {
+            key = $1;
+            sub(":$", "", key);
+            val = $2;
+            for (i=3; i<=NF; i++) {
+                val = val " " $i};
+                data[controller, key] = val;
+        }
+        END {
+            # Using the 1st seen controller. Should we select specific instead?
+            power_status = data[controllers[1], "Powered"];
+            if (ctrl_count > 0) {
+                if (power_status == "no") {
+                    power_status = "off"
+                } else if (power_status == "yes") {
+                    power_status = "on"
+                } else {
+                    printf("Unexpected bluetooth power status: %s\n", power_status)\
+                        > "/dev/stderr";
+                    power_status = "ERROR"
+                }
+            } else {
+                power_status = "off"  # TODO: Perhaps use differentiated marker?
+            }
+            printf("%s\n", power_status);
+        }'
+}
+
+produce_screen_brightness() {
+    screen_brightness_device_path="$1"
+    echo "\
+        $(cat $screen_brightness_device_path/max_brightness) \
+        $(cat $screen_brightness_device_path/brightness)\
+    "
+}
+
+produce_volume() {
+    pactl list sinks \
+    | awk '
+        /^\tMute:/ {
+            printf("%s,", $0);
+        }
+        /^\tVolume:/ {
+            for (i=2; i<=NF; i++) printf(" %s", $i);
+        }' \
+    | awk -v RS=',' '
+        /^[ \t]*Mute:/        {mute  = $2}
+        /^[ \t]*front-left:/  {left  = $4}
+        /^[ \t]*front-right:/ {right = $4}
+        END {
+            if (mute == "yes") {
+                print("x")
+            } else {
+                print("%s %s\n", left, right)
+            }
+        }
+        '
+}
+
+produce_mpd_state() {
+    echo 'status' \
+    | nc 127.0.0.1 6600 \
+    | awk '
+        {
+            status[$1] = $2
+        }
+
+        /^time: +[0-9]+:[0-9]+$/ {
+            split($2, time, ":")
+            seconds_current = time[1]
+            seconds_total   = time[2]
+
+            hours = int(seconds_current / 60 / 60);
+            secs_beyond_hours = seconds_current - (hours * 60 * 60);
+            mins = int(secs_beyond_hours / 60);
+            secs = secs_beyond_hours - (mins * 60);
+            if (hours > 0) {
+                current_time = sprintf("%d:%.2d:%.2d", hours, mins, secs)
+            } else {
+                current_time = sprintf("%.2d:%.2d", mins, secs)
+            }
+
+            if (seconds_total > 0) {
+                time_percentage = (seconds_current / seconds_total) * 100
+                current_percentage = sprintf("%d%%", time_percentage)
+            } else {
+                current_percentage = "~"
+            }
+        }
+
+        END {
+            state = status["state:"]
+
+            if (state == "play") {
+                symbol = "▶"
+            } else if (state == "pause") {
+                symbol = "❚❚"
+            } else if (state == "stop") {
+                symbol = "⬛"
+            } else {
+                symbol = "--"
+            }
+
+            printf(\
+                "%s %s %s\n",
+                status["state:"], current_time, current_percentage\
+            )
+        }
+        '
+}
+
+produce_mpd_song() {
+    echo 'currentsong' \
+    | nc 127.0.0.1 6600 \
+    | awk '
+        /^OK/ {
+            next
+        }
+
+        {
+            key = $1
+            sub("^" key " +", "")
+            val = $0
+            data[key] = val
+        }
+
+        END {
+            name  = data["Name:"]
+            title = data["Title:"]
+            file  = data["file:"]
+
+            if (name) {
+                out = name
+            } else if (title) {
+                out = title
+            } else if (file) {
+                last = split(file, parts, "/")
+                out = parts[last]
+            } else {
+                out = ""
+            }
+            print out
+        }
+        '
+}
+
+produce_weather() {
+    weather_station_id="$1"
+    metar -d "$weather_station_id" 2>&1 \
     | awk '
         /METAR pattern not found in NOAA data/ {
             failures++
@@ -23,38 +281,211 @@ fetch_weather() {
         }'
 }
 
-fetch_datetime() {
+produce_datetime() {
     date +'%a %b %d %H:%M:%S'
 }
 
-read_and_react() {
+consume() {
     pipe="$1"
+    debug="$2"
+    prefixes_of_net_interfaces_to_show="$3"
     tail -f "$pipe" \
     | stdbuf -o L awk \
+        -v opt_debug="$debug" \
+        -v opt_mpd_song_max_chars=10 \
+        -v opt_prefixes_of_net_interfaces_to_show="$prefixes_of_net_interfaces_to_show" \
         '
-            /^in:WEATHER:/\
+            /^in:ENERGY/\
             {
-                chop_off_msg_tag()
-                db["weather_temperature"] = $0
+                split_msg_parts()
+                db["energy_state"]      = $1
+                db["energy_percentage"] = $2
             }
 
-            /^in:DATE_TIME:/\
+            /^in:MEMORY/\
             {
-                chop_off_msg_tag()
-                db["datetime"] = $0
+                split_msg_parts()
+                db["memory_total"] = $1
+                db["memory_used"]  = $2
             }
 
-            /^out:BAR:/\
+            /^in:FAN +status:/\
             {
-                chop_off_msg_tag()
+                split_msg_parts()
+                db["fan_status"] = $2
+            }
+
+            /^in:FAN +speed:/\
+            {
+                split_msg_parts()
+                db["fan_speed"] = $2
+            }
+
+            /^in:FAN +level:/\
+            {
+                split_msg_parts()
+                db["fan_level"] = $2
+            }
+
+            /^in:TEMPERATURE/\
+            {
+                split_msg_parts()
+                db["temperature"] = $1
+            }
+
+            /^in:LOAD_AVG/\
+            {
+                split_msg_parts()
+                set_load_avg()
+            }
+
+            /^in:DISK_IO/\
+            {
+                split_msg_parts()
+                set_disk_io()
+            }
+
+            /^in:DISK_SPACE/\
+            {
+                split_msg_parts()
+                db["disk_space_used"] = msg_body
+            }
+
+            /^in:NET_ADDR_IO/\
+            {
+                split_msg_parts()
+                set_net_addr_io()
+            }
+
+            /^in:NET_WIFI_STATUS/\
+            {
+                split_msg_parts()
+                db["net_wifi_status"] = msg_body
+            }
+
+            /^in:BLUETOOTH_POWER/\
+            {
+                split_msg_parts()
+                db["bluetooth_power"] = msg_body
+            }
+
+            /^in:SCREEN_BRIGHTNESS/\
+            {
+                split_msg_parts()
+                set_screen_brightness()
+            }
+
+            /^in:VOLUME/\
+            {
+                split_msg_parts()
+                db["volume"] = msg_body
+            }
+
+            /^in:MPD_STATE/\
+            {
+                split_msg_parts()
+                db["mpd_state"]             = $1
+                db["mpd_curr_song_time"]    = $2
+                db["mpd_curr_song_percent"] = $3
+            }
+
+            /^in:MPD_SONG/\
+            {
+                split_msg_parts()
+                db["mpd_curr_song_name"] = msg_body
+            }
+
+            /^in:WEATHER/\
+            {
+                split_msg_parts()
+                db["weather_temperature"] = msg_body
+            }
+
+            /^in:DATE_TIME/\
+            {
+                split_msg_parts()
+                db["datetime"] = msg_body
+            }
+
+            /^out:BAR/\
+            {
+                split_msg_parts()
                 print make_bar()
             }
 
-            function chop_off_msg_tag() {
-                sub("^" $1 " +", "")
+            function set_load_avg(    sched) {
+                split($4, sched, "/")
+                db["load_avg_1min"]             = $1
+                db["load_avg_5min"]             = $2
+                db["load_avg_15min"]            = $3
+                db["kern_sched_queue_runnable"] = sched[1]
+                db["kern_sched_queue_total"]    = sched[2]
+                db["kern_sched_latest_pid"]     = $5
+            }
+
+            function set_disk_io(    curr_w, curr_r, prev_w, prev_r) {
+                curr_w = $1
+                curr_r = $2
+                prev_w = db["disk_io_curr_w"]
+                prev_r = db["disk_io_curr_r"]
+                db["disk_io_curr_w"] = curr_w
+                db["disk_io_curr_r"] = curr_r
+                db["disk_io_diff_w"] = curr_w - prev_w
+                db["disk_io_diff_r"] = curr_r - prev_r
             }
 
-            function make_bar(    position, bar, i) {
+            function set_net_addr_io(    \
+                interface, address, io_curr_w, io_curr_r, io_prev_w, io_prev_r\
+            ) {
+                interface = $1
+                address   = $2
+                io_curr_w = $3
+                io_curr_r = $4
+                if (interface) {
+                    if (address && io_curr_w && io_curr_r) {
+                        # recalculate
+                        io_prev_w = net_io_curr_w[interface]
+                        io_prev_r = net_io_curr_r[interface]
+
+                        net_addr[interface]      = address
+                        net_io_curr_w[interface] = io_curr_w
+                        net_io_curr_r[interface] = io_curr_r
+                        net_io_diff_w[interface] = io_curr_w - io_prev_w
+                        net_io_diff_r[interface] = io_curr_r - io_prev_r
+                    } else {
+                        # clear
+                        net_addr[interface]      = ""
+                        net_io_curr_w[interface] = 0
+                        net_io_curr_r[interface] = 0
+                        net_io_diff_w[interface] = 0
+                        net_io_diff_r[interface] = 0
+                    }
+                }
+            }
+
+            function set_screen_brightness(    max, cur) {
+                max = $1
+                cur = $2
+                db["screen_brightness"] = (cur / max) * 100
+            }
+
+            function split_msg_parts() {
+                msg_head = $1
+                sub("^" msg_head " +", "")
+                msg_body = $0
+                debug(msg_head, msg_body)
+            }
+
+            function make_bar(    position, bar, sep, i, j) {
+                position[++i] = make_status_energy()
+                position[++i] = make_status_mem()
+                position[++i] = make_status_cpu()
+                position[++i] = make_status_disk()
+                position[++i] = make_status_net()
+                position[++i] = sprintf("B=%s", db["bluetooth_power"])
+                position[++i] = sprintf("*%d%%", db["screen_brightness"])
+                position[++i] = sprintf("(%s)", db["volume"])
+                position[++i] = make_status_mpd()
                 position[++i] = db["weather_temperature"]
                 position[++i] = db["datetime"]
                 bar = ""
@@ -65,38 +496,295 @@ read_and_react() {
                 }
                 return bar
             }
+
+            function make_status_energy(    state, direction_of_change) {
+                state = db["energy_state"]
+                if (state == "discharging") {
+                    direction_of_change = "<"
+                } else if (state == "charging") {
+                    direction_of_change = ">"
+                } else {
+                    direction_of_change = "="
+                };
+                printf("E%s%s", direction_of_change, db["energy_percentage"])
+            }
+
+            function make_status_mem(    total, used, percent, status) {
+                total = db["memory_total"]
+                used  = db["memory_used"]
+                # To avoid division by zero
+                if (total && used) {
+                    percent = round((used / total) * 100)
+                    status = sprintf("%d%%", percent)
+                } else {
+                    status = "__"
+                }
+                return sprintf("M=%s", status)
+            }
+
+            function make_status_cpu(    load, temp, fan) {
+                load = db["load_avg_1min"]
+                temp = db["temperature"] / 1000
+                fan  = db["fan_speed"]
+                return sprintf("C=[%4.2f %d°C %4drpm]", load, temp, fan)
+            }
+
+            function make_status_disk(    bytes_per_sector, bytes_per_mb, w, r) {
+                bytes_per_sector = 512
+                bytes_per_mb     = 1024 * 1024
+                w = (db["disk_io_diff_w"] * bytes_per_sector) / bytes_per_mb
+                r = (db["disk_io_diff_r"] * bytes_per_sector) / bytes_per_mb
+                return \
+                    sprintf("D=[%s %0.3f▲ %0.3f▼]", db["disk_space_used"], w, r)
+            }
+
+            function make_status_net(    \
+                out,
+                number_of_interfaces_to_show,
+                n,
+                array_of_prefixes_of_interfaces_to_show,
+                prefix,
+                interface,
+                label,
+                count_printed,
+                sep,
+                io_stat,
+                dw, dr,
+                bytes_per_unit\
+            ) {
+                out = ""
+                number_of_interfaces_to_show = \
+                    split(\
+                        opt_prefixes_of_net_interfaces_to_show,\
+                        array_of_prefixes_of_interfaces_to_show,\
+                        ","\
+                    )
+                for (n = 1; n <= number_of_interfaces_to_show; n++) {
+                    prefix = array_of_prefixes_of_interfaces_to_show[n]
+                    for (interface in net_addr) {
+                        if (interface ~ ("^" prefix)) {
+                            label = substr(interface, 1, 1)
+                            if (net_addr[interface]) {
+                                bytes_per_mb = 1024 * 1024  # TODO: option
+                                dw = net_io_diff_w[interface] / bytes_per_mb
+                                dr = net_io_diff_r[interface] / bytes_per_mb
+                                io_stat = sprintf("%0.3f▲ %0.3f▼", dw, dr)
+                            } else {
+                                io_stat = "--"
+                            }
+                            if (interface ~ "^w") {
+                                label = label ":" db["net_wifi_status"]
+                            }
+                            if (++count_printed > 1) {
+                                sep = "  "
+                            } else {
+                                sep = ""
+                            }
+                            out = out sep label ":" io_stat
+                        }
+                    }
+                }
+                return sprintf("N[%s]", out)
+            }
+
+            function make_status_mpd(    state, status) {
+                state = db["mpd_state"]
+
+                if (state == "play") {
+                    status = make_status_mpd_state_known("▶")
+                } else if (state == "pause") {
+                    status = make_status_mpd_state_known("❚❚")
+                } else if (state == "stop") {
+                    status = make_status_mpd_state_known("⬛")
+                } else {
+                    status = make_status_mpd_state_unknown("--")
+                }
+
+                return sprintf("[%s]", status)
+            }
+
+            function make_status_mpd_state_known(symbol) {
+                return sprintf(\
+                    "%s %s %s %s",
+                    symbol,
+                    db["mpd_curr_song_time"],
+                    db["mpd_curr_song_percent"],
+                    substr(db["mpd_curr_song_name"], 1, opt_mpd_song_max_chars)\
+                )
+            }
+
+            function make_status_mpd_state_unknown(symbol) {
+                return sprintf("%s", symbol)
+            }
+
+            function round(n) {
+                return int(n + 0.5)
+            }
+
+            function debug(location, msg) {
+                if (opt_debug) {
+                    print_error(location, msg)
+                }
+            }
+
+            function print_error(location, msg) {
+                print(location " ==> " msg) > "/dev/stderr"
+            }
         '
 }
 
-trigger_bar() {
+produce_bar_req() {
     echo ''
 }
 
 spawn() {
     cmd="$1"
     pipe="$2"
-    tag="$3"
+    msg_head="$3"
     interval="$4"
     while true; do
-        echo "${tag}${MSG_TAG_SEP}$($cmd)" > "$pipe"
-        sleep  "$interval"
+        $cmd | while read line; do
+            echo "${msg_head} $line" > "$pipe"
+        done
+        sleep "$interval"
     done &
 }
 
 main() {
-    dir_bin="$1"
-    dir_data="$2"
-    pipe="$dir_data/pipe"
+    # Defaults
+    debug=0
+    dir_data="$HOME/.khatus"
+    weather_station_id='KJFK'
+    screen_brightness_device_name='acpi_video0'
+    prefixes_of_net_interfaces_to_show='w'  # comma-separated
+    disk_space_device='/'
+    disk_io_device='sda'
+    thermal_zone=0
+    fan_path='/proc/acpi/ibm/fan'
+
+    # User-overrides
+    long_options=''
+    long_options+='debug'
+    long_options+=',data-dir:'
+    long_options+=',weather-station:'
+    long_options+=',screen-device:'
+    long_options+=',prefixes_of_net_interfaces_to_show:'
+    long_options+=',disk_space_device:'
+    long_options+=',thermal_zone:'
+    long_options+=',fan_path:'
+    OPTS=$(
+        getopt \
+            -o 'd' \
+            -l $long_options \
+            -- "$@"
+    )
+    eval set -- "$OPTS"
+    while true
+    do
+        case "$1" in
+            -d|--debug)
+                debug=1
+                shift
+                ;;
+            --data-dir)
+                dir_data="$2"
+                shift 2
+                ;;
+            --weather-station)
+                weather_station_id="$2"
+                shift 2
+                ;;
+            --screen-device)
+                screen_brightness_device_name="$2"
+                shift 2
+                ;;
+            --prefixes_of_net_interfaces_to_show)
+                prefixes_of_net_interfaces_to_show="$2"
+                shift 2
+                ;;
+            --disk_space_device)
+                disk_space_device="$2"
+                shift 2
+                ;;
+            --disk_io_device)
+                disk_io_device="$2"
+                shift 2
+                ;;
+            --thermal_zone)
+                thermal_zone="$2"
+                shift 2
+                ;;
+            --fan_path)
+                fan_path="$2"
+                shift 2
+                ;;
+            --)
+                shift
+                break
+                ;;
+        esac
+    done
 
-    WEATHER_STATION_ID='KJFK'
+    pipe="$dir_data/khatus_data_pipe"
+    screen_brightness_device_path='/sys/class/backlight'
+    screen_brightness_device_path+="/$screen_brightness_device_name"
 
+    ( echo "Khatus starting with the following parameters:"
+      ( echo "    debug|= $debug"
+        echo "    dir_data|= $dir_data"
+        echo "    pipe|= $pipe"
+        echo "    screen_brightness_device_name|= $screen_brightness_device_name"
+        echo "    screen_brightness_device_path|= $screen_brightness_device_path"
+        echo "    weather_station_id|= $weather_station_id"
+        echo "    prefixes_of_net_interfaces_to_show|= $prefixes_of_net_interfaces_to_show"
+        echo "    disk_space_device|= $disk_space_device"
+        echo "    disk_io_device|= $disk_io_device"
+        echo "    thermal_zone|= $thermal_zone"
+        echo "    fan_path|= $fan_path"
+      ) | column -ts\|
+      echo ''
+    ) >&2
+
+    mkdir -p "$dir_data"
     rm -f "$pipe"
     mkfifo "$pipe"
 
-    spawn fetch_datetime "$pipe" 'in:DATE_TIME' 1
-    spawn fetch_weather  "$pipe" 'in:WEATHER'   $(( 30 * 60 ))
-    spawn trigger_bar    "$pipe" 'out:BAR'      1
-    read_and_react       "$pipe"
+    cmd_produce_screen_brightness='produce_screen_brightness'
+    cmd_produce_screen_brightness+=" $screen_brightness_device_path"
+
+    cmd_produce_weather="produce_weather $weather_station_id"
+
+    cmd_produce_disk_space="produce_disk_space $disk_space_device"
+
+    cmd_produce_disk_io="produce_disk_io $disk_io_device"
+
+    cmd_produce_temperature="produce_temperature $thermal_zone"
+
+    cmd_produce_fan="produce_fan $fan_path"
+
+    # TODO: Redirect each worker's stderr to a dedicated log file
+    spawn produce_datetime                 "$pipe" 'in:DATE_TIME' 1
+    spawn "$cmd_produce_screen_brightness" "$pipe" 'in:SCREEN_BRIGHTNESS' 1
+    spawn "$cmd_produce_weather"           "$pipe" 'in:WEATHER'   $(( 30 * 60 ))
+    spawn produce_mpd_state                "$pipe" 'in:MPD_STATE' 1
+    spawn produce_mpd_song                 "$pipe" 'in:MPD_SONG'  1
+    spawn produce_volume                   "$pipe" 'in:VOLUME'    1
+    spawn produce_bluetooth_power          "$pipe" 'in:BLUETOOTH_POWER' 5
+    spawn produce_net_wifi_status          "$pipe" 'in:NET_WIFI_STATUS' 5
+    spawn produce_net_addr_io              "$pipe" 'in:NET_ADDR_IO' 1
+    spawn "$cmd_produce_disk_space"        "$pipe" 'in:DISK_SPACE' 1
+    spawn "$cmd_produce_disk_io"           "$pipe" 'in:DISK_IO' 1
+    spawn produce_loadavg                  "$pipe" 'in:LOAD_AVG' 1
+    spawn "$cmd_produce_temperature"       "$pipe" 'in:TEMPERATURE' 1
+    spawn "$cmd_produce_fan"               "$pipe" 'in:FAN' 1
+    spawn produce_memory                   "$pipe" 'in:MEMORY' 1
+    spawn produce_energy                   "$pipe" 'in:ENERGY' 1
+    spawn produce_bar_req                  "$pipe" 'out:BAR'      1
+
+    consume \
+      "$pipe" \
+      "$debug" \
+      "$prefixes_of_net_interfaces_to_show"
 }
 
 main $@
This page took 0.053737 seconds and 4 git commands to generate.