#! /usr/bin/awk -f /^OK/ { debug("OK line", $0) } /^ERROR/ { debug("ERROR line", $0) shift() msg_head = $1 shift() msg_body = $0 alert_trigger_hi(msg_head, "KhatusSensorError", msg_body) } /^OK in:ENERGY battery/\ { debug("ENERGY battery", $0) sub("%$", "", $5) db["energy_state_prev"] = db["energy_state_curr"] db["energy_state_curr"] = $4 db["energy_percentage"] = ensure_numeric($5) alert_check_energy_battery() } /^OK in:ENERGY line_power/\ { debug("ENERGY line_power", $0) db["energy_line_power_prev"] = db["energy_line_power_curr"] db["energy_line_power_curr"] = $4 alert_check_energy_line_power() } /^OK in:MEMORY/\ { shift() shift() db["memory_total"] = $1 db["memory_used"] = $2 } /^OK in:FAN +status:/\ { shift() shift() db["fan_status"] = $2 } /^OK in:FAN +speed:/\ { shift() shift() db["fan_speed"] = $2 } /^OK in:FAN +level:/\ { shift() shift() db["fan_level"] = $2 } /^OK in:TEMPERATURE/\ { shift() shift() db["temperature"] = $1 } /^OK in:LOAD_AVG/\ { shift() shift() set_load_avg() } /^OK in:DISK_IO/\ { shift() shift() set_disk_io() } /^OK in:DISK_SPACE/\ { shift() shift() db["disk_space_used"] = $0 } /^OK in:NET_ADDR_IO/\ { shift() shift() set_net_addr_io() } /^OK in:NET_WIFI_STATUS/\ { shift() shift() set_net_wifi_status() } /^OK in:BLUETOOTH_POWER/\ { shift() shift() db["bluetooth_power"] = $0 } /^OK in:SCREEN_BRIGHTNESS/\ { shift() shift() set_screen_brightness() } /^OK in:VOLUME/\ { shift() set_volume() } /^OK in:MPD_SONG OK +MPD/ { delete db_mpd_song; next } /^OK in:MPD_SONG OK$/ { set_mpd_playing() ; next } /^OK in:MPD_SONG / { set_mpd_song() ; next } /^OK in:MPD_STATE /\ { shift() shift() db["mpd_status_state"] = $1 db["mpd_status_time"] = $2 db["mpd_status_percent"] = $3 } /^OK in:WEATHER/\ { shift() shift() db["weather_temperature"] = $0 } /^OK in:DATE_TIME/\ { shift() shift() db["datetime"] = $0 output_msg_status_bar(make_status_bar()) } function set_volume( mute, left, right) { # 0 RUNNING no 75% 75% #msg_head = $1 #sink = $2 #state = $3 mute = $4 left = $5 right = $6 if (mute == "no") { db["volume"] = sprintf("%s %s", left, right) } else if (mute == "yes") { db["volume"] = "X" } else { error("set_volume", "Unexpected value for 'mute' field: " mute) } } function set_mpd_song( key, val) { shift() key = $2 shift() shift() val = $0 db_mpd_song[key] = val debug("set_mpd_song", "", db_mpd_song) } function set_mpd_playing( \ currently_playing, name, title, file, last, parts\ ) { debug("set_mpd_playing", "", db_mpd_song) name = db_mpd_song["Name:"] title = db_mpd_song["Title:"] file = db_mpd_song["file:"] if (name) { currently_playing = name } else if (title) { currently_playing = title } else if (file) { last = split(file, parts, "/") currently_playing = parts[last] } else { currently_playing = "" } db["mpd_playing_prev"] = db["mpd_playing_curr"] db["mpd_playing_curr"] = currently_playing alert_check_mpd() } function alert_check_mpd( curr, prev, name, body) { prev = db["mpd_playing_prev"] curr = db["mpd_playing_curr"] if (curr && curr != prev) { name = db_mpd_song["Name:"] if (name) { body = name } else { body = \ db_mpd_song["Artist:"] \ " - " db_mpd_song["Album:"] \ " - " db_mpd_song["Title:"] } alert_trigger_low("alert_check_mpd", "MpdNowPlaying", body) } } # TODO: Generalize alert spec lang # - trigger threshold # - above/bellow/equal to threshold value # - priority # - snooze time (if already alerted, when to re-alert?) # - text: subject/body function alert_check_energy_battery( \ from, dbg, state_curr, state_prev, remaining, subj, body\ ) { from = "alert_check_energy_battery" state_curr = db["energy_state_curr"] state_prev = db["energy_state_prev"] remaining = db["energy_percentage"] dbg["state_curr"] = state_curr dbg["remaining"] = remaining debug(from, "", dbg) if (state_curr == "discharging") { if (remaining < 5) { subj = "Energy_CRITICALLY_Low" body = sprintf("%d%% CHARGE NOW!!! GO GO GO!!!", remaining) alert_trigger_hi(from, subj, body) } else if (remaining < 10) { subj = "Energy_Very_Low" body = sprintf("%d%% Plug it in ASAP.", remaining) alert_trigger_hi(from, subj, body) } else if (remaining < 15) { subj = "Energy_Low" body = sprintf("%d%% Get the charger.", remaining) alert_trigger_hi(from, subj, body) } else if (remaining < 20) { subj = "Energy_Low" body = sprintf("%d%% Get the charger.", remaining) alert_trigger_med(from, subj, body) } else if (remaining < 50) { if (!state__alerts__energy__notified_bellow_half) { state__alerts__energy__notified_bellow_half = 1 subj = "Energy_Bellow_Half" body = sprintf("%d%% Where is the charger?", remaining) alert_trigger_med(from, subj, body) } } } else { # TODO: Reconsider the competing global-state organizing-conventions state__alerts__energy__notified_bellow_half = 0 } } function alert_check_energy_line_power( \ from, dbg, line_power_curr, line_power_prev, subj, body \ ) { from = "alert_check_energy_line_power" dbg["energy_line_power_prev"] = db["energy_line_power_prev"] dbg["energy_line_power_curr"] = db["energy_line_power_curr"] debug(from, "", dbg) line_power_curr = db["energy_line_power_curr"] line_power_prev = db["energy_line_power_prev"] if (line_power_curr == "no" && line_power_prev != "no") { alert_trigger_low(from, "PowerUnplugged", "") } } function alert_trigger_low(from, subject, body) { alert_trigger("low", from, subject, body) } function alert_trigger_med(from, subject, body) { alert_trigger("med", from, subject, body) } function alert_trigger_hi(from, subject, body) { alert_trigger("hi", from, subject, body) } function alert_trigger(priority, from, subject, body, msg) { # priority : "low" | "med" | "hi" # subject : no spaces # body : anything msg = sprintf("khatus_%s %s %s %s", from, priority, subject, body) output_msg_alert(msg) } function output_msg_alert(msg) { # TODO: Should alerts go into a dedicated channel? output_msg("ALERT", msg, "/dev/stdout") } function output_msg_status_bar(msg) { output_msg("STATUS_BAR", msg, "/dev/stdout") } function output_msg(type, content, channel) { print(type, content) > channel } 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 set_net_wifi_status( interface) { interface = $1 shift() db["net_wifi_status", interface] = $0 } 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 } # TODO: Revise overuse of shift() where it is not really needed function shift() { sub("^" $1 " +", "") } function make_status_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 = "" sep = "" for (j = 1; j <= i; j++) { bar = bar sep position[j] sep = " " } return bar } function make_status_energy( state, direction_of_change) { state = db["energy_state_curr"] if (state == "discharging") { direction_of_change = "<" } else if (state == "charging") { direction_of_change = ">" } else { direction_of_change = "=" }; return sprintf("E%s%d%%", 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 when data is missing 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", interface] } 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_status_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_status_time"], db["mpd_status_percent"], substr(db["mpd_playing_curr"], 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, values, sep, vals, key, payload) { if (opt_debug) { sep = "" vals = "" for (key in values) { vals = sprintf("%s%s%s: %s", vals, sep, key, values[key]) sep = ", " } payload = \ sprintf("LOCATION[%s] MSG[%s] DATA[%s]", location, msg, vals) output_msg("DEBUG", payload, "/dev/stderr") } } function error(location, msg) { # TODO: Reconsider classifying internal errors as alerts # Maybe better to keep the error class distinct and provide a # an optional transformation from error to alert alert_trigger_hi(location, "KhatusControllerError", msg) } function ensure_numeric(n) { return n + 0 } #------------------------------- # Why do we need ensure_numeric? #------------------------------- # awk appears to be guessing the type of an inputted scalar based on usage, so # if we read-in a number, but did not use it in any numeric operations, but did # use as a string (even in just a format string!) - it will be treated as a # string and can lead to REALLY SURPRISING behavior in conditional statements, # where smaller number may compare as greater than the bigger ones, such as. # # Demo: # # $ awk 'BEGIN {x = "75"; y = "100"; sprintf("x: %d, y: %d\n", x, y); if (x > y) {print "75 > 100"} else if (x < y) {print "75 < 100"}}' # 75 < 100 # $ awk 'BEGIN {x = "75"; y = "100"; sprintf("x: %s, y: %d\n", x, y); if (x > y) {print "75 > 100"} else if (x < y) {print "75 < 100"}}' # 75 > 100 # However, once used as a number, seems to stay that way even after being # used as string: # # $ awk 'BEGIN {x = "75"; y = "100"; x + y; sprintf("x: %s, y: %d\n", x, y); if (x > y) {print "75 > 100"} else if (x < y) {print "75 < 100"}}' # 75 < 100 # # $ awk 'BEGIN {x = "75"; y = "100"; x + y; sprintf("x: %s, y: %d\n", x, y); z = x y; if (x > y) {print "75 > 100"} else if (x < y) {print "75 < 100"}}' # 75 < 100 # # $ awk 'BEGIN {x = "75"; y = "100"; x + y; z = x y; if (x > y) {print "75 > 100"} else if (x < y) {print "75 < 100"}}' # 75 < 100 # $ awk 'BEGIN {x = "75"; y = "100"; z = x y; if (x > y) {print "75 > 100"} else if (x < y) {print "75 < 100"}}' # 75 > 100