Clear MPD state on non-0 exit from MPD song sensor
[khatus.git] / bin / khatus_controller
1 #! /usr/bin/awk -f
2
3 /^OK/ { debug("OK line", $0) }
4
5 /^ERROR in:MPD.*NON_ZERO_EXIT_CODE/ {
6 for (mpd_key in db) {
7 if (mpd_key ~ "^mpd_") {
8 delete db[mpd_key]
9 }
10 }
11 next
12 }
13
14 /^ERROR/ {
15 debug("ERROR line", $0)
16 shift()
17 msg_head = $1
18 shift()
19 msg_body = $0
20 alert_trigger_hi(msg_head, "KhatusSensorError", msg_body)
21 }
22
23 /^OK in:ENERGY battery/\
24 {
25 debug("ENERGY battery", $0)
26 sub("%$", "", $5)
27 db["energy_state_prev"] = db["energy_state_curr"]
28 db["energy_state_curr"] = $4
29 db["energy_percentage"] = ensure_numeric($5)
30 alert_check_energy_battery()
31 }
32
33 /^OK in:ENERGY line_power/\
34 {
35 debug("ENERGY line_power", $0)
36 db["energy_line_power_prev"] = db["energy_line_power_curr"]
37 db["energy_line_power_curr"] = $4
38 alert_check_energy_line_power()
39 }
40
41 /^OK in:MEMORY/\
42 {
43 shift()
44 shift()
45 db["memory_total"] = $1
46 db["memory_used"] = $2
47 }
48
49 /^OK in:FAN +status:/\
50 {
51 shift()
52 shift()
53 db["fan_status"] = $2
54 }
55
56 /^OK in:FAN +speed:/\
57 {
58 shift()
59 shift()
60 db["fan_speed"] = $2
61 }
62
63 /^OK in:FAN +level:/\
64 {
65 shift()
66 shift()
67 db["fan_level"] = $2
68 }
69
70 /^OK in:TEMPERATURE/\
71 {
72 shift()
73 shift()
74 db["temperature"] = $1
75 }
76
77 /^OK in:LOAD_AVG/\
78 {
79 shift()
80 shift()
81 set_load_avg()
82 }
83
84 /^OK in:DISK_IO/\
85 {
86 shift()
87 shift()
88 set_disk_io()
89 }
90
91 /^OK in:DISK_SPACE/\
92 {
93 shift()
94 shift()
95 db["disk_space_used"] = $0
96 }
97
98 /^OK in:NET_ADDR_IO/\
99 {
100 shift()
101 shift()
102 set_net_addr_io()
103 }
104
105 /^OK in:NET_WIFI_STATUS/\
106 {
107 shift()
108 shift()
109 set_net_wifi_status()
110 }
111
112 /^OK in:BLUETOOTH_POWER/\
113 {
114 shift()
115 shift()
116 db["bluetooth_power"] = $0
117 }
118
119 /^OK in:SCREEN_BRIGHTNESS/\
120 {
121 shift()
122 shift()
123 set_screen_brightness()
124 }
125
126 /^OK in:VOLUME/\
127 {
128 shift()
129 set_volume()
130 }
131
132 /^OK in:MPD_SONG OK +MPD/ { delete db_mpd_song; next }
133 /^OK in:MPD_SONG OK$/ { set_mpd_playing() ; next }
134 /^OK in:MPD_SONG / { set_mpd_song() ; next }
135
136 /^OK in:MPD_STATE /\
137 {
138 shift()
139 shift()
140 db["mpd_status_state"] = $1
141 db["mpd_status_time"] = $2
142 db["mpd_status_percent"] = $3
143 }
144
145 /^OK in:WEATHER temperature/\
146 {
147 shift()
148 shift()
149 shift()
150 db["weather_temperature"] = $0
151 }
152
153 /^OK in:WEATHER phenomenon/\
154 {
155 shift()
156 shift()
157 shift()
158 alert_trigger_low("weather_phenomenon", "WeatherPhenomenon", $0)
159 }
160
161 /^OK in:DATE_TIME/\
162 {
163 shift()
164 shift()
165 db["datetime"] = $0
166 output_msg_status_bar(make_status_bar())
167 }
168
169 function set_volume( mute, left, right) {
170 # 0 RUNNING no 75% 75%
171 #msg_head = $1
172 #sink = $2
173 #state = $3
174 mute = $4
175 left = $5
176 right = $6
177
178 if (mute == "no") {
179 db["volume"] = sprintf("%s %s", left, right)
180 } else if (mute == "yes") {
181 db["volume"] = "X"
182 } else {
183 error("set_volume", "Unexpected value for 'mute' field: " mute)
184 }
185 }
186
187 function set_mpd_song( key, val) {
188 shift()
189 key = $2
190 shift()
191 shift()
192 val = $0
193 db_mpd_song[key] = val
194 debug("set_mpd_song", "", db_mpd_song)
195 }
196
197 function set_mpd_playing( \
198 currently_playing, name, title, file, last, parts\
199 ) {
200 debug("set_mpd_playing", "", db_mpd_song)
201 name = db_mpd_song["Name:"]
202 title = db_mpd_song["Title:"]
203 file = db_mpd_song["file:"]
204
205 if (name) {
206 currently_playing = name
207 } else if (title) {
208 currently_playing = title
209 } else if (file) {
210 last = split(file, parts, "/")
211 currently_playing = parts[last]
212 } else {
213 currently_playing = ""
214 }
215 db["mpd_playing_prev"] = db["mpd_playing_curr"]
216 db["mpd_playing_curr"] = currently_playing
217
218 alert_check_mpd()
219 }
220
221 function alert_check_mpd( curr, prev, name, body) {
222 prev = db["mpd_playing_prev"]
223 curr = db["mpd_playing_curr"]
224 if (curr && curr != prev) {
225 name = db_mpd_song["Name:"]
226 if (name) {
227 body = name
228 } else {
229 body = \
230 db_mpd_song["Artist:"] \
231 " - " db_mpd_song["Album:"] \
232 " - " db_mpd_song["Title:"]
233 }
234 alert_trigger_low("alert_check_mpd", "MpdNowPlaying", body)
235 }
236 }
237
238 # TODO: Generalize alert spec lang
239 # - trigger threshold
240 # - above/bellow/equal to threshold value
241 # - priority
242 # - snooze time (if already alerted, when to re-alert?)
243 # - text: subject/body
244 function alert_check_energy_battery( \
245 from, dbg, state_curr, state_prev, remaining, subj, body\
246 ) {
247 from = "alert_check_energy_battery"
248
249 state_curr = db["energy_state_curr"]
250 state_prev = db["energy_state_prev"]
251 remaining = db["energy_percentage"]
252
253 dbg["state_curr"] = state_curr
254 dbg["remaining"] = remaining
255 debug(from, "", dbg)
256
257 if (state_curr == "discharging") {
258 if (remaining < 5) {
259 subj = "Energy_CRITICALLY_Low"
260 body = sprintf("%d%% CHARGE NOW!!! GO GO GO!!!", remaining)
261 alert_trigger_hi(from, subj, body)
262 } else if (remaining < 10) {
263 subj = "Energy_Very_Low"
264 body = sprintf("%d%% Plug it in ASAP.", remaining)
265 alert_trigger_hi(from, subj, body)
266 } else if (remaining < 15) {
267 subj = "Energy_Low"
268 body = sprintf("%d%% Get the charger.", remaining)
269 alert_trigger_hi(from, subj, body)
270 } else if (remaining < 20) {
271 subj = "Energy_Low"
272 body = sprintf("%d%% Get the charger.", remaining)
273 alert_trigger_med(from, subj, body)
274 } else if (remaining < 50) {
275 if (!state__alerts__energy__notified_bellow_half) {
276 state__alerts__energy__notified_bellow_half = 1
277 subj = "Energy_Bellow_Half"
278 body = sprintf("%d%% Where is the charger?", remaining)
279 alert_trigger_med(from, subj, body)
280 }
281 }
282 } else {
283 # TODO: Reconsider the competing global-state organizing-conventions
284 state__alerts__energy__notified_bellow_half = 0
285 }
286 }
287
288 function alert_check_energy_line_power( \
289 from, dbg, line_power_curr, line_power_prev, subj, body \
290 ) {
291 from = "alert_check_energy_line_power"
292
293 dbg["energy_line_power_prev"] = db["energy_line_power_prev"]
294 dbg["energy_line_power_curr"] = db["energy_line_power_curr"]
295 debug(from, "", dbg)
296
297 line_power_curr = db["energy_line_power_curr"]
298 line_power_prev = db["energy_line_power_prev"]
299
300 if (line_power_curr == "no" && line_power_prev != "no") {
301 alert_trigger_low(from, "PowerUnplugged", "")
302 }
303 }
304
305 function alert_trigger_low(from, subject, body) {
306 alert_trigger("low", from, subject, body)
307 }
308
309 function alert_trigger_med(from, subject, body) {
310 alert_trigger("med", from, subject, body)
311 }
312
313 function alert_trigger_hi(from, subject, body) {
314 alert_trigger("hi", from, subject, body)
315 }
316
317 function alert_trigger(priority, from, subject, body, msg) {
318 # priority : "low" | "med" | "hi"
319 # subject : no spaces
320 # body : anything
321 msg = sprintf("khatus_%s %s %s %s", from, priority, subject, body)
322 output_msg_alert(msg)
323 }
324
325 function output_msg_alert(msg) {
326 # TODO: Should alerts go into a dedicated channel?
327 output_msg("ALERT", msg, "/dev/stdout")
328 }
329
330 function output_msg_status_bar(msg) {
331 output_msg("STATUS_BAR", msg, "/dev/stdout")
332 }
333
334 function output_msg(type, content, channel) {
335 print(type, content) > channel
336 }
337
338 function set_load_avg( sched) {
339 split($4, sched, "/")
340 db["load_avg_1min"] = $1
341 db["load_avg_5min"] = $2
342 db["load_avg_15min"] = $3
343 db["kern_sched_queue_runnable"] = sched[1]
344 db["kern_sched_queue_total"] = sched[2]
345 db["kern_sched_latest_pid"] = $5
346 }
347
348 function set_disk_io( curr_w, curr_r, prev_w, prev_r) {
349 curr_w = $1
350 curr_r = $2
351 prev_w = db["disk_io_curr_w"]
352 prev_r = db["disk_io_curr_r"]
353 db["disk_io_curr_w"] = curr_w
354 db["disk_io_curr_r"] = curr_r
355 db["disk_io_diff_w"] = curr_w - prev_w
356 db["disk_io_diff_r"] = curr_r - prev_r
357 }
358
359 function set_net_wifi_status( interface) {
360 interface = $1
361 shift()
362 db["net_wifi_status", interface] = $0
363 }
364
365 function set_net_addr_io( \
366 interface, address, io_curr_w, io_curr_r, io_prev_w, io_prev_r\
367 ) {
368 interface = $1
369 address = $2
370 io_curr_w = $3
371 io_curr_r = $4
372 if (interface) {
373 if (address && io_curr_w && io_curr_r) {
374 # recalculate
375 io_prev_w = net_io_curr_w[interface]
376 io_prev_r = net_io_curr_r[interface]
377
378 net_addr[interface] = address
379 net_io_curr_w[interface] = io_curr_w
380 net_io_curr_r[interface] = io_curr_r
381 net_io_diff_w[interface] = io_curr_w - io_prev_w
382 net_io_diff_r[interface] = io_curr_r - io_prev_r
383 } else {
384 # clear
385 net_addr[interface] = ""
386 net_io_curr_w[interface] = 0
387 net_io_curr_r[interface] = 0
388 net_io_diff_w[interface] = 0
389 net_io_diff_r[interface] = 0
390 }
391 }
392 }
393
394 function set_screen_brightness( max, cur) {
395 max = $1
396 cur = $2
397 db["screen_brightness"] = (cur / max) * 100
398 }
399
400 # TODO: Revise overuse of shift() where it is not really needed
401 function shift() {
402 sub("^" $1 " +", "")
403 }
404
405 function make_status_bar( position, bar, sep, i, j) {
406 position[++i] = make_status_energy()
407 position[++i] = make_status_mem()
408 position[++i] = make_status_cpu()
409 position[++i] = make_status_disk()
410 position[++i] = make_status_net()
411 position[++i] = sprintf("B=%s", db["bluetooth_power"])
412 position[++i] = sprintf("*%d%%", db["screen_brightness"])
413 position[++i] = sprintf("(%s)", db["volume"])
414 position[++i] = make_status_mpd()
415 position[++i] = db["weather_temperature"]
416 position[++i] = db["datetime"]
417 bar = ""
418 sep = ""
419 for (j = 1; j <= i; j++) {
420 bar = bar sep position[j]
421 sep = " "
422 }
423 return bar
424 }
425
426 function make_status_energy( state, direction_of_change) {
427 state = db["energy_state_curr"]
428 if (state == "discharging") {
429 direction_of_change = "<"
430 } else if (state == "charging") {
431 direction_of_change = ">"
432 } else {
433 direction_of_change = "="
434 };
435 return sprintf("E%s%d%%", direction_of_change, db["energy_percentage"])
436 }
437
438 function make_status_mem( total, used, percent, status) {
439 total = db["memory_total"]
440 used = db["memory_used"]
441 # To avoid division by zero when data is missing
442 if (total && used) {
443 percent = round((used / total) * 100)
444 status = sprintf("%d%%", percent)
445 } else {
446 status = "__"
447 }
448 return sprintf("M=%s", status)
449 }
450
451 function make_status_cpu( load, temp, fan) {
452 load = db["load_avg_1min"]
453 temp = db["temperature"] / 1000
454 fan = db["fan_speed"]
455 return sprintf("C=[%4.2f %d°C %4drpm]", load, temp, fan)
456 }
457
458 function make_status_disk( bytes_per_sector, bytes_per_mb, w, r) {
459 bytes_per_sector = 512
460 bytes_per_mb = 1024 * 1024
461 w = (db["disk_io_diff_w"] * bytes_per_sector) / bytes_per_mb
462 r = (db["disk_io_diff_r"] * bytes_per_sector) / bytes_per_mb
463 return \
464 sprintf("D=[%s %0.3f▲ %0.3f▼]", db["disk_space_used"], w, r)
465 }
466
467 function make_status_net( \
468 out,
469 number_of_interfaces_to_show,
470 n,
471 array_of_prefixes_of_interfaces_to_show,
472 prefix,
473 interface,
474 label,
475 count_printed,
476 sep,
477 io_stat,
478 dw, dr,
479 bytes_per_unit\
480 ) {
481 out = ""
482 number_of_interfaces_to_show = \
483 split(\
484 opt_prefixes_of_net_interfaces_to_show,\
485 array_of_prefixes_of_interfaces_to_show,\
486 ","\
487 )
488 for (n = 1; n <= number_of_interfaces_to_show; n++) {
489 prefix = array_of_prefixes_of_interfaces_to_show[n]
490 for (interface in net_addr) {
491 if (interface ~ ("^" prefix)) {
492 label = substr(interface, 1, 1)
493 if (net_addr[interface]) {
494 bytes_per_mb = 1024 * 1024 # TODO: option
495 dw = net_io_diff_w[interface] / bytes_per_mb
496 dr = net_io_diff_r[interface] / bytes_per_mb
497 io_stat = sprintf("%0.3f▲ %0.3f▼", dw, dr)
498 } else {
499 io_stat = "--"
500 }
501 if (interface ~ "^w") {
502 label = label ":" db["net_wifi_status", interface]
503 }
504 if (++count_printed > 1) {
505 sep = " "
506 } else {
507 sep = ""
508 }
509 out = out sep label ":" io_stat
510 }
511 }
512 }
513 return sprintf("N[%s]", out)
514 }
515
516 function make_status_mpd( state, status) {
517 state = db["mpd_status_state"]
518
519 if (state == "play") {
520 status = make_status_mpd_state_known("▶")
521 } else if (state == "pause") {
522 status = make_status_mpd_state_known("❚❚")
523 } else if (state == "stop") {
524 status = make_status_mpd_state_known("⬛")
525 } else {
526 status = make_status_mpd_state_unknown("--")
527 }
528
529 return sprintf("[%s]", status)
530 }
531
532 function make_status_mpd_state_known(symbol) {
533 return sprintf(\
534 "%s %s %s %s",
535 symbol,
536 db["mpd_status_time"],
537 db["mpd_status_percent"],
538 substr(db["mpd_playing_curr"], 1, opt_mpd_song_max_chars)\
539 )
540 }
541
542 function make_status_mpd_state_unknown(symbol) {
543 return sprintf("%s", symbol)
544 }
545
546 function round(n) {
547 return int(n + 0.5)
548 }
549
550 function debug(location, msg, values, sep, vals, key, payload) {
551 if (opt_debug) {
552 sep = ""
553 vals = ""
554 for (key in values) {
555 vals = sprintf("%s%s%s: %s", vals, sep, key, values[key])
556 sep = ", "
557 }
558 payload = \
559 sprintf("LOCATION[%s] MSG[%s] DATA[%s]", location, msg, vals)
560 output_msg("DEBUG", payload, "/dev/stderr")
561 }
562 }
563
564 function error(location, msg) {
565 # TODO: Reconsider classifying internal errors as alerts
566 # Maybe better to keep the error class distinct and provide a
567 # an optional transformation from error to alert
568 alert_trigger_hi(location, "KhatusControllerError", msg)
569 }
570
571 function ensure_numeric(n) {
572 return n + 0
573 }
574 #-------------------------------
575 # Why do we need ensure_numeric?
576 #-------------------------------
577 # awk appears to be guessing the type of an inputted scalar based on usage, so
578 # if we read-in a number, but did not use it in any numeric operations, but did
579 # use as a string (even in just a format string!) - it will be treated as a
580 # string and can lead to REALLY SURPRISING behavior in conditional statements,
581 # where smaller number may compare as greater than the bigger ones, such as.
582 #
583 # Demo:
584 #
585 # $ 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"}}'
586 # 75 < 100
587 # $ 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"}}'
588 # 75 > 100
589
590 # However, once used as a number, seems to stay that way even after being
591 # used as string:
592 #
593 # $ 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"}}'
594 # 75 < 100
595 #
596 # $ 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"}}'
597 # 75 < 100
598 #
599 # $ awk 'BEGIN {x = "75"; y = "100"; x + y; z = x y; if (x > y) {print "75 > 100"} else if (x < y) {print "75 < 100"}}'
600 # 75 < 100
601 # $ awk 'BEGIN {x = "75"; y = "100"; z = x y; if (x > y) {print "75 > 100"} else if (x < y) {print "75 < 100"}}'
602 # 75 > 100
This page took 0.141179 seconds and 4 git commands to generate.