├── README ├── mdash ├── mpanel ├── mprune ├── mqttcore.py └── mwatch /README: -------------------------------------------------------------------------------- 1 | This is a collection of MQTT utilities. 2 | 3 | mwatch is a handy utility to monitor MQTT activity. 4 | mprune is a utility to prune the MQTT topic tree. Removing retained topics. 5 | mdash is a utility to monitor the stats in an MQTT broker ($SYS/# topics) 6 | mpanel is a utility to monitor the status of clients in an MQTT broker. (Note: this utility depends on a standardized set of topics published from each client.) 7 | 8 | 9 | -------------------------------------------------------------------------------- /mdash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # mdash - Watches $SYS topics from an MQTT broker and presents them in a dashboard view. 4 | # 5 | # By Dennis Sell 6 | # 7 | # 8 | 9 | 10 | 11 | __author__ = "Dennis Sell" 12 | __copyright__ = "Copyright (C) Dennis Sell" 13 | 14 | 15 | #BUGS 16 | # resizing terminal crashes. Possibly some curses/mosquitto interaction. 17 | 18 | 19 | import os 20 | import sys 21 | import getopt 22 | import mosquitto 23 | import curses 24 | from curses.ascii import isprint 25 | import time 26 | import datetime 27 | 28 | version_number = 0.5 29 | topiclist = [] 30 | count = [] 31 | numtopics = 0 32 | rxcount = 0 33 | pos = 0 34 | col = 0 35 | topic_col = 0 36 | topic_size = 60 37 | retained_col = topic_size 38 | qos_col = topic_size 39 | count_col = topic_size 40 | timestamp_col = topic_size 41 | message_col = topic_size 42 | items = [] 43 | 44 | class item: 45 | def __init__(self, topic, y, x, length, rl = 'r'): 46 | self.topic = topic 47 | self.y = y 48 | self.x = x 49 | self.length = length 50 | self.rl = rl 51 | 52 | 53 | def usage(): 54 | print "usage: mdash [options]" #[hostname[:port]] [topics]" 55 | print "Watch topics change from an MQTT broker." 56 | print 57 | print " -u --hostname HOSTNAME set hostname for broker (default localhost)" 58 | print " -p --port PORT set port for mqtt broker (default 1883)" 59 | print " -h --help show this help information" 60 | print " -v --version show version information" 61 | print "By Dennis Sell -- 2013" 62 | 63 | 64 | def version(): 65 | print "version: " + str(version_number) 66 | 67 | 68 | def show_help(): 69 | help_cols = curses.COLS/2 70 | help_lines = curses.LINES/2 71 | help_win = curses.newwin(24, 50, help_lines-12, help_cols-25) 72 | help_win.box() 73 | help_win.addstr(1, 1, "Key Commands", curses.A_UNDERLINE) 74 | help_win.addstr(3, 1, "[Home] - Jump to top left") 75 | help_win.addstr(4, 1, "b - Jump to begining of line") 76 | help_win.addstr(5, 1, "e - Jump to end of line") 77 | help_win.addstr(6, 1, "[Right Arrow] - Scroll to right") 78 | help_win.addstr(7, 1, "[Left Arrow] - Scroll to left") 79 | help_win.addstr(8, 1, "[Up Arrow] - Scroll up") 80 | help_win.addstr(9, 1, "[Down Arrow] - Scroll down") 81 | help_win.addstr(10, 1, "[Page Up] - Page up") 82 | help_win.addstr(11, 1, "[Page Down] - Page down") 83 | help_win.addstr(16, 1, "q - To exit help.") 84 | help_win.noutrefresh() 85 | curses.doupdate() 86 | while True: 87 | key = stdscr.getch() 88 | if key == ord("q"): 89 | break 90 | help_win.erase() 91 | del help_win 92 | stats.nooutrefresh() 93 | stdscr.nooutrefresh() 94 | curses.doupdate() 95 | 96 | 97 | def on_connect(mosq, userdata, rc): 98 | stdscr.addstr(curses.LINES-1,0,"Connected to %s" % broker) 99 | stdscr.noutrefresh() 100 | curses.doupdate() 101 | 102 | 103 | def on_message(mosq, userdata, msg): 104 | global numtopics 105 | global rxcount 106 | global retained_flag 107 | global timestamp_flag 108 | 109 | for i in items: 110 | if i.topic == msg.topic: 111 | if i.length + 1 > len(msg.payload) and i.rl == 'r': 112 | stdscr.addstr(i.y, i.x-1, str(msg.payload).rjust(i.length)) 113 | if msg.topic == "$SYS/broker/uptime": 114 | t = msg.payload.split(" ") 115 | uptime = datetime.timedelta(seconds=int(t[0])) 116 | uptime_text = str(uptime).rjust(12) 117 | stdscr.addstr(14,other_col+other_tab+2, uptime_text) 118 | curses.doupdate() 119 | 120 | 121 | def getTerminalSize(): 122 | """ 123 | returns (lines:int, cols:int) 124 | """ 125 | import os 126 | def ioctl_GWINSZ(fd): 127 | import fcntl, termios 128 | return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) 129 | # try stdin, stdout, stderr 130 | for fd in (0, 1, 2): 131 | try: 132 | return ioctl_GWINSZ(fd) 133 | except: 134 | pass 135 | # try os.ctermid() 136 | try: 137 | fd = os.open(os.ctermid(), os.O_RDONLY) 138 | try: 139 | return ioctl_GWINSZ(fd) 140 | finally: 141 | os.close(fd) 142 | except: 143 | pass 144 | # try `stty size` 145 | try: 146 | return tuple(int(x) for x in os.popen("stty size", "r").read().split()) 147 | except: 148 | pass 149 | # try environment variables 150 | try: 151 | return tuple(int(os.getenv(var)) for var in ("LINES", "COLUMNS")) 152 | except: 153 | pass 154 | # i give up. return default. 155 | return (25, 80) 156 | 157 | 158 | try: 159 | opts, args = getopt.getopt(sys.argv[1:], 'u:p:hv', ['hostname=', 'port=', 'help', 'version']) 160 | except getopt.GetoptError as err: 161 | print str(err) 162 | usage() 163 | sys.exit(2) 164 | 165 | 166 | # defaults 167 | broker = "127.0.0.1" 168 | port = 1883 169 | topics = "$SYS/#" 170 | 171 | for opt, arg in opts: 172 | if opt in ('-h', '--help'): 173 | usage() 174 | sys.exit(2) 175 | if opt in ('-v', '--version'): 176 | version() 177 | sys.exit(2) 178 | elif opt in ('-u', '--hostname'): 179 | broker = arg 180 | elif opt in ('-p', '--port'): 181 | port = int(arg) 182 | else: 183 | usage() 184 | sys.exit(2) 185 | 186 | 187 | # minitialize mosquitto, connect, and subscribe 188 | client = mosquitto.Mosquitto() 189 | client.on_message = on_message 190 | client.on_connect = on_connect 191 | client.connect(broker, port) 192 | client.subscribe(topics) 193 | 194 | # initialize curses 195 | stdscr = curses.initscr() 196 | if curses.COLS < 116: 197 | curses.endwin() 198 | print "mwatch requires a minimum terminal width of 116 columns." 199 | sys.exit(2) 200 | if curses.LINES < 24: 201 | curses.endwin() 202 | print "mwatch requires a minimum terminal heigth of 24 rows." 203 | sys.exit(2) 204 | stdscr.nodelay(1) 205 | curses.curs_set(0) 206 | if curses.has_colors(): 207 | curses.start_color() 208 | curses.cbreak() 209 | curses.noecho() 210 | stdscr.keypad(1) 211 | 212 | 213 | # draw basic screen 214 | stdscr.addstr("MQTT Dashboard ", curses.A_REVERSE) 215 | stdscr.addstr(curses.LINES-1,curses.COLS-37,"? for help, q to Quit") 216 | stats = curses.newwin(curses.LINES-2,curses.COLS,1,0) 217 | stats.box() 218 | stdscr.noutrefresh() 219 | stats.noutrefresh() 220 | clients_col = 3 221 | clients_tab = 10 222 | messages_col = 32 223 | messages_tab = 10 224 | other_col = 62 225 | other_tab = 12 226 | sent1m_col = 18 227 | rec1m_col = 32 228 | sent5m_col = 41 229 | rec5m_col = 55 230 | sent15m_col = 64 231 | rec15m_col = 79 232 | senttot_col = 89 233 | rectot_col = 105 234 | 235 | 236 | stdscr.addstr(3, 1, "Version:", curses.A_UNDERLINE) 237 | stdscr.addstr(5, 1, "Load: Per 1 Min. Per 5 Min. Per 15 Min. Total") 238 | stdscr.addstr(6, 1, " Sent Received Sent Received Sent Received Sent Received", curses.A_UNDERLINE) 239 | stdscr.addstr(7, 1, "Bytes:") 240 | stdscr.addstr(8, 1, "Messages:") 241 | stdscr.addstr(9, 1, "Published:") 242 | stdscr.addstr(10, 1, "Connections:") 243 | stdscr.addstr(11, 1, "Sockets:") 244 | stdscr.addstr(13, 1, "Clients:") 245 | stdscr.addstr(14, clients_col, "Active:") 246 | stdscr.addstr(15, clients_col, "Inactive:") 247 | stdscr.addstr(16, clients_col, "Total:") 248 | stdscr.addstr(17, clients_col, "Expired:") 249 | stdscr.addstr(18, clients_col, "Maximum:") 250 | stdscr.addstr(13, 30, "Messages:") 251 | stdscr.addstr(14, messages_col, "Retained:") 252 | stdscr.addstr(15, messages_col, "Stored:") 253 | stdscr.addstr(16, messages_col, "Dropped:") 254 | stdscr.addstr(17, messages_col, "Inflight:") 255 | stdscr.addstr(18, messages_col, "Subscrip.:") 256 | stdscr.addstr(13, 60, "Other:") 257 | stdscr.addstr(14, other_col, "Uptime:") 258 | stdscr.addstr(15, other_col, "Heap Cur.:") 259 | stdscr.addstr(16, other_col, "Heap Max.:") 260 | stdscr.addstr(20, 1, "Log:") 261 | stats.noutrefresh() 262 | curses.doupdate() 263 | 264 | items.append(item("$SYS/broker/version", 3, 10, 25)) 265 | items.append(item("$SYS/broker/timestamp", 3, 38, 25)) 266 | items.append(item("$SYS/broker/changeset", 3, 67, 25)) 267 | 268 | 269 | #items.append(item("$SYS/broker/publish/bytes/received", 7, rectot_col, 8)) 270 | #items.append(item("$SYS/broker/publish/bytes/sent", 7, senttot_col, 8)) 271 | 272 | items.append(item("$SYS/broker/load/bytes/received/1min", 7, rec1m_col, 8)) 273 | items.append(item("$SYS/broker/load/bytes/received/5min", 7, rec5m_col, 8)) 274 | items.append(item("$SYS/broker/load/bytes/received/15min", 7, rec15m_col, 8)) 275 | items.append(item("$SYS/broker/load/bytes/sent/1min", 7, sent1m_col, 8)) 276 | items.append(item("$SYS/broker/load/bytes/sent/5min", 7, sent5m_col, 8)) 277 | items.append(item("$SYS/broker/load/bytes/sent/15min", 7, sent15m_col, 8)) 278 | items.append(item("$SYS/broker/bytes/received", 7, rectot_col, 10)) 279 | items.append(item("$SYS/broker/bytes/sent", 7, senttot_col, 10)) 280 | 281 | items.append(item("$SYS/broker/load/messages/received/1min", 8, rec1m_col, 8)) 282 | items.append(item("$SYS/broker/load/messages/received/5min", 8, rec5m_col, 8)) 283 | items.append(item("$SYS/broker/load/messages/received/15min", 8, rec15m_col, 8)) 284 | items.append(item("$SYS/broker/load/messages/sent/1min", 8, sent1m_col, 8)) 285 | items.append(item("$SYS/broker/load/messages/sent/5min", 8, sent5m_col, 8)) 286 | items.append(item("$SYS/broker/load/messages/sent/15min", 8, sent15m_col, 8)) 287 | items.append(item("$SYS/broker/messages/received", 8, rectot_col, 10)) 288 | items.append(item("$SYS/broker/messages/sent", 8, senttot_col, 10)) 289 | 290 | items.append(item("$SYS/broker/load/publish/received/1min", 9, rec1m_col, 8)) 291 | items.append(item("$SYS/broker/load/publish/received/5min", 9, rec5m_col, 8)) 292 | items.append(item("$SYS/broker/load/publish/received/15min", 9, rec15m_col, 8)) 293 | items.append(item("$SYS/broker/load/publish/sent/1min", 9, sent1m_col, 8)) 294 | items.append(item("$SYS/broker/load/publish/sent/5min", 9, sent5m_col, 8)) 295 | items.append(item("$SYS/broker/load/publish/sent/15min", 9, sent15m_col, 8)) 296 | items.append(item("$SYS/broker/publish/messages/received", 9, rectot_col, 10)) 297 | items.append(item("$SYS/broker/publish/messages/sent", 9, senttot_col, 10)) 298 | 299 | items.append(item("$SYS/broker/load/connections/1min", 10, sent1m_col+5, 8)) 300 | items.append(item("$SYS/broker/load/connections/5min", 10, sent5m_col+5, 8)) 301 | items.append(item("$SYS/broker/load/connections/15min", 10, sent15m_col+5, 8)) 302 | 303 | items.append(item("$SYS/broker/load/sockets/1min", 11, sent1m_col+5, 8)) 304 | items.append(item("$SYS/broker/load/sockets/5min", 11, sent5m_col+5, 8)) 305 | items.append(item("$SYS/broker/load/sockets/15min", 11, sent15m_col+5, 8)) 306 | 307 | items.append(item("$SYS/broker/retained messages/count", 14, messages_col+messages_tab, 8)) 308 | items.append(item("$SYS/broker/messages/stored", 15, messages_col+messages_tab, 8)) 309 | items.append(item("$SYS/broker/messages/dropped", 16, messages_col+messages_tab, 8)) 310 | items.append(item("$SYS/broker/messages/inflight", 17, messages_col+messages_tab, 8)) 311 | items.append(item("$SYS/broker/subscriptions/count", 18, messages_col+messages_tab, 8)) 312 | 313 | items.append(item("$SYS/broker/clients/active", 14, clients_col+clients_tab, 8)) 314 | items.append(item("$SYS/broker/clients/inactive", 15, clients_col+clients_tab, 8)) 315 | items.append(item("$SYS/broker/clients/total", 16, clients_col+clients_tab, 8)) 316 | items.append(item("$SYS/broker/clients/expired", 17,clients_col+clients_tab, 8)) 317 | items.append(item("$SYS/broker/clients/maximum", 18, clients_col+clients_tab, 8)) 318 | 319 | items.append(item("$SYS/broker/heap/current size", 15, other_col+other_tab, 10)) 320 | items.append(item("$SYS/broker/heap/maximum size", 16, other_col+other_tab, 10)) 321 | 322 | 323 | # main loop 324 | while client.loop(0) == 0: 325 | key = stdscr.getch() 326 | if key == ord("q"): 327 | break 328 | if key == ord("?"): 329 | show_help() 330 | if key == curses.KEY_DOWN: 331 | pos = pos + 1 332 | if pos > numtopics: 333 | pos = numtopics 334 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 335 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 336 | curses.doupdate() 337 | if key == curses.KEY_UP: 338 | pos = pos - 1 339 | if pos < 0: 340 | pos = 0 341 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 342 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 343 | curses.doupdate() 344 | if key == curses.KEY_NPAGE: 345 | pos = pos + page - 2 346 | if pos > numtopics: 347 | pos = numtopics 348 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 349 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 350 | curses.doupdate() 351 | if key == curses.KEY_PPAGE: 352 | pos = pos - page + 2 353 | if pos < 0: 354 | pos = 0 355 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 356 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 357 | curses.doupdate() 358 | if key == curses.KEY_LEFT: 359 | col = col - 1 360 | if col < 0: 361 | col = 0 362 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 363 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 364 | curses.doupdate() 365 | if key == curses.KEY_RIGHT: 366 | col = col + 1 367 | if col > 2003-curses.COLS: 368 | col = 2003-curses.COLS 369 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 370 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 371 | curses.doupdate() 372 | if key == curses.KEY_HOME: 373 | pos = 0 374 | col = 0 375 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 376 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 377 | curses.doupdate() 378 | if key == ord("b"): 379 | col = 0 380 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 381 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 382 | curses.doupdate() 383 | if key == ord("e"): 384 | col = 2003 - curses.COLS 385 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 386 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 387 | curses.doupdate() 388 | if key == curses.KEY_RESIZE: #Resizing the terminal screen crashes this program. 389 | l, c = getTerminalSize() #It seems to be something to do with calling the mosquitto library in the above while loop. 390 | if c > 50: #Redrawing belongs here if the problem gets solved. 391 | stdscr.addstr(0, curses.COLS-20, "Col: " + str(c) + " Lines: " + str(l)) 392 | l, c = getTerminalSize() 393 | if c > 120: 394 | stdscr.addstr(0,c - 27,"%s" % str(datetime.datetime.now()), curses.A_REVERSE) 395 | pass 396 | 397 | 398 | # clean up terminal settings 399 | curses.nocbreak() 400 | stdscr.keypad(0) 401 | curses.echo() 402 | curses.endwin() 403 | 404 | -------------------------------------------------------------------------------- /mpanel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # mpanel - Watches client status information from an MQTT broker 4 | # 5 | # By Dennis Sell 6 | # 7 | # 8 | 9 | 10 | 11 | __author__ = "Dennis Sell" 12 | __copyright__ = "Copyright (C) Dennis Sell" 13 | 14 | 15 | #BUGS 16 | # resizing terminal crashes. Possibly some curses/mosquitto interaction. 17 | 18 | 19 | import os 20 | import sys 21 | import getopt 22 | import mosquitto 23 | import curses 24 | from curses.ascii import isprint 25 | import time 26 | import datetime 27 | 28 | 29 | version_number = 0.8 30 | topiclist = [] 31 | numclients = 0 32 | rxcount = 0 33 | pos = 0 34 | col = 0 35 | topic_col = 0 36 | topic_size = 60 37 | retained_col = topic_size 38 | qos_col = topic_size 39 | count_col = topic_size 40 | timestamp_col = topic_size 41 | message_col = topic_size 42 | items = [] 43 | 44 | 45 | class item: 46 | def __init__(self, topic_tail, x, length, rl = 'l'): 47 | self.topic_tail = topic_tail 48 | self.x = x 49 | self.length = length 50 | self.rl = rl 51 | 52 | 53 | def usage(): 54 | print "usage: mpanel [options]" #[hostname[:port]] [topics]" 55 | print "Watch topics change from an MQTT broker." 56 | print 57 | print " -u --hostname HOSTNAME set hostname for broker (default localhost)" 58 | print " -p --port PORT set port for mqtt broker (default 1883)" 59 | print " -t --topics TOPICS set topics to watch (default /#)" 60 | print " -h --help show this help information" 61 | print " -v --version show version information" 62 | print "By Dennis Sell -- 2013" 63 | 64 | 65 | def version(): 66 | print "version: " + str(version_number) 67 | 68 | 69 | def show_help(): 70 | help_cols = curses.COLS/2 71 | help_lines = curses.LINES/2 72 | help_win = curses.newwin(24, 70, help_lines-12, help_cols-35) 73 | help_win.box() 74 | help_win.addstr(1, 1, "Key Commands", curses.A_UNDERLINE) 75 | help_win.addstr(3, 1, "[Home] - Jump to top left") 76 | help_win.addstr(4, 1, "b - Jump to begining of line") 77 | help_win.addstr(5, 1, "e - Jump to end of line") 78 | help_win.addstr(6, 1, "[Right Arrow] - Scroll to right") 79 | help_win.addstr(7, 1, "[Left Arrow] - Scroll to left") 80 | help_win.addstr(8, 1, "[Up Arrow] - Scroll up") 81 | help_win.addstr(9, 1, "[Down Arrow] - Scroll down") 82 | help_win.addstr(10, 1, "[Page Up] - Page up") 83 | help_win.addstr(11, 1, "[Page Down] - Page down") 84 | help_win.addstr(13, 1, "p - Send a ping request globally") 85 | help_win.addstr(14, 1, "i - Send an identify request globally") 86 | help_win.addstr(15, 1, "R - Reload client settings for a specific client") 87 | help_win.addstr(17, 1, "q - To exit help.") 88 | help_win.noutrefresh() 89 | curses.doupdate() 90 | while True: 91 | key = stdscr.getch() 92 | if key == ord("q"): 93 | break 94 | help_win.erase() 95 | del help_win 96 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 97 | header_txt.refresh(0, 0, 2, 1, 2, curses.COLS-3) 98 | stdscr.nooutrefresh() 99 | curses.doupdate() 100 | 101 | 102 | def get_client(prompt): 103 | client_cols = curses.COLS/2 104 | client_lines = curses.LINES/2 105 | client_win = curses.newwin(7, 50, client_lines-12, client_cols-25) 106 | client_win.box() 107 | client_win.addstr(1, 1, prompt) 108 | client_win.addstr(3, 1, "Enter client number:") 109 | client_win.addstr(5, 1, "0 - For none") 110 | client_win.noutrefresh() 111 | stdscr.nooutrefresh() 112 | curses.doupdate() 113 | curses.echo() 114 | client_win.addstr(3, 22, " ", curses.A_UNDERLINE) 115 | client_num = client_win.getstr(3, 22) 116 | curses.noecho() 117 | client_win.erase() 118 | del client_win 119 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 120 | header_txt.refresh(0, 0, 2, 1, 2, curses.COLS-3) 121 | stdscr.nooutrefresh() 122 | curses.doupdate() 123 | if client_num == '0': 124 | ret = 99999 125 | elif client_num.isdigit(): 126 | ret = int(client_num) - 1 127 | if ret < 0: 128 | ret = 99999 129 | else: 130 | ret = 99999 131 | return ret 132 | 133 | 134 | def on_connect(mosq, userdata, rc): 135 | stdscr.addstr(curses.LINES-1,0,"Connected to %s" % broker) 136 | stdscr.addstr(0,15,"Sunscribed to %s" % topics, curses.A_REVERSE) 137 | stdscr.noutrefresh() 138 | curses.doupdate() 139 | 140 | 141 | def on_message(mosq, userdata, msg): 142 | global numclients 143 | global rxcount 144 | global retained_flag 145 | global timestamp_flag 146 | 147 | rxcount = rxcount + 1 148 | stdscr.addstr(0,80,"Msg count %s" % rxcount, curses.A_REVERSE) 149 | t = msg.topic.split("/") 150 | if len(t) > 3: 151 | if t[2] != "global": 152 | if t[2] not in topiclist: 153 | topiclist.append(t[2]) 154 | numclients = numclients + 1 155 | offset = topiclist.index(t[2]) 156 | stats_txt.addstr(offset, num_col-1, str(numclients).rjust(num_len)) 157 | stats_txt.addstr(offset, name_col-1, t[2].ljust(name_len)) 158 | stdscr.addstr(0,60,"Client count %s" % numclients, curses.A_REVERSE) 159 | else: 160 | offset = topiclist.index(t[2]) 161 | for i in items: 162 | if i.topic_tail == t[3]: 163 | if i.length + 1 > len(msg.payload) and i.rl == 'r': 164 | stats_txt.addstr(offset, i.x-1, str(msg.payload).rjust(i.length)) 165 | else: 166 | stats_txt.addstr(offset, i.x-1, str(msg.payload).ljust(i.length)) 167 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 168 | curses.doupdate() 169 | 170 | 171 | def getTerminalSize(): 172 | """ 173 | returns (lines:int, cols:int) 174 | """ 175 | import os, struct 176 | def ioctl_GWINSZ(fd): 177 | import fcntl, termios 178 | return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) 179 | # try stdin, stdout, stderr 180 | for fd in (0, 1, 2): 181 | try: 182 | return ioctl_GWINSZ(fd) 183 | except: 184 | pass 185 | # try os.ctermid() 186 | try: 187 | fd = os.open(os.ctermid(), os.O_RDONLY) 188 | try: 189 | return ioctl_GWINSZ(fd) 190 | finally: 191 | os.close(fd) 192 | except: 193 | pass 194 | # try `stty size` 195 | try: 196 | return tuple(int(x) for x in os.popen("stty size", "r").read().split()) 197 | except: 198 | pass 199 | # try environment variables 200 | try: 201 | return tuple(int(os.getenv(var)) for var in ("LINES", "COLUMNS")) 202 | except: 203 | pass 204 | # i give up. return default. 205 | return (25, 80) 206 | 207 | 208 | try: 209 | opts, args = getopt.getopt(sys.argv[1:], 'u:p:t:hv', ['hostname=', 'port=', 'topics=', 'help', 'version']) 210 | except getopt.GetoptError as err: 211 | print str(err) 212 | usage() 213 | sys.exit(2) 214 | 215 | 216 | # defaults 217 | broker = "127.0.0.1" 218 | port = 1883 219 | topics = "/clients/#" 220 | 221 | for opt, arg in opts: 222 | if opt in ('-h', '--help'): 223 | usage() 224 | sys.exit(2) 225 | if opt in ('-v', '--version'): 226 | version() 227 | sys.exit(2) 228 | elif opt in ('-u', '--hostname'): 229 | broker = arg 230 | elif opt in ('-p', '--port'): 231 | port = int(arg) 232 | elif opt in ('-t', '--topics'): 233 | topics = arg 234 | else: 235 | usage() 236 | sys.exit(2) 237 | 238 | 239 | # minitialize mosquitto, connect, and subscribe 240 | client = mosquitto.Mosquitto() 241 | client.on_message = on_message 242 | client.on_connect = on_connect 243 | client.connect(broker, port) 244 | client.subscribe(topics) 245 | 246 | # initialize curses 247 | stdscr = curses.initscr() 248 | if curses.COLS < 95: 249 | curses.endwin() 250 | print "mpanel requires a minimum terminal width of 95 columns." 251 | sys.exit(2) 252 | if curses.LINES < 7: 253 | curses.endwin() 254 | print "mpanel requires a minimum terminal heigth of 7 rows." 255 | sys.exit(2) 256 | stdscr.nodelay(1) 257 | curses.curs_set(0) 258 | if curses.has_colors(): 259 | curses.start_color() 260 | curses.cbreak() 261 | curses.noecho() 262 | stdscr.keypad(1) 263 | 264 | 265 | # draw basic screen 266 | stdscr.addstr("MQTT Panel ", curses.A_REVERSE) 267 | stdscr.chgat(-1,curses.A_REVERSE) 268 | stdscr.addstr(curses.LINES-1,curses.COLS-57,"page-up, page-dn, cursor arrows, ? for help, q to Quit") 269 | page = curses.LINES - 3 270 | stats = curses.newwin(curses.LINES-2,curses.COLS,1,0) 271 | stats.box() 272 | header_txt = curses.newpad(1,2000) 273 | stats_txt = curses.newpad(2000,2000) 274 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 275 | stdscr.noutrefresh() 276 | stats.noutrefresh() 277 | 278 | num_col = 1 279 | num_len = 3 280 | name_col = num_col+num_len+1 281 | name_len = 34 282 | ver_col = name_col+name_len+1 283 | ver_len = 14 284 | status_col = ver_col+ver_len+1 285 | status_len = 13 286 | class_col = status_col+status_len+1 287 | class_len = 6 288 | ping_col = class_col+class_len+1 289 | ping_len = 8 290 | extip_col = ping_col+ping_len+1 291 | extip_len = 15 292 | locip_col = extip_col+extip_len+1 293 | locip_len = 15 294 | pid_col = locip_col+locip_len+1 295 | pid_len = 6 296 | startts_col = pid_col+pid_len+1 297 | startts_len = 26 298 | con_col = startts_col+startts_len+1 299 | con_len = 4 300 | conts_col = con_col+con_len+1 301 | conts_len = 26 302 | dists_col = conts_col+conts_len+1 303 | dists_len = 26 304 | 305 | header_txt.addstr(0, num_col, "#".ljust(num_len), curses.A_UNDERLINE) 306 | header_txt.addstr(0, name_col, "Client Name".ljust(name_len), curses.A_UNDERLINE) 307 | header_txt.addstr(0, ver_col, "Ver. (a:c:d)".ljust(ver_len), curses.A_UNDERLINE) 308 | header_txt.addstr(0, status_col, "Status".ljust(status_len), curses.A_UNDERLINE) 309 | header_txt.addstr(0, class_col, "Class".ljust(class_len), curses.A_UNDERLINE) 310 | header_txt.addstr(0, ping_col, "Ping".ljust(ping_len), curses.A_UNDERLINE) 311 | header_txt.addstr(0, extip_col, "External Ip".ljust(extip_len), curses.A_UNDERLINE) 312 | header_txt.addstr(0, locip_col, "Local IP".ljust(locip_len), curses.A_UNDERLINE) 313 | header_txt.addstr(0, pid_col, "PID".ljust(pid_len), curses.A_UNDERLINE) 314 | header_txt.addstr(0, startts_col, "Start Timestamp".ljust(startts_len), curses.A_UNDERLINE) 315 | header_txt.addstr(0, con_col, "Con.".ljust(con_len), curses.A_UNDERLINE) 316 | header_txt.addstr(0, conts_col, "Connect Timestamp".ljust(conts_len), curses.A_UNDERLINE) 317 | header_txt.addstr(0, dists_col, "Disconnect Timestamp".ljust(dists_len), curses.A_UNDERLINE) 318 | header_txt.refresh(0, 0, 2, 1, 2, curses.COLS-3) 319 | stats.noutrefresh() 320 | curses.doupdate() 321 | 322 | items.append(item("status", status_col, status_len, 'l')) 323 | items.append(item("ping", ping_col, ping_len, 'l')) 324 | items.append(item("version", ver_col, 4, 'r')) 325 | items.append(item("class", class_col, class_len, 'l')) 326 | items.append(item("core-version", ver_col+5, 4, 'r')) 327 | items.append(item("daemon-version", ver_col+10, 4, 'r')) 328 | items.append(item("extip", extip_col, extip_len, 'r')) 329 | items.append(item("locip", locip_col, locip_len, 'r')) 330 | items.append(item("pid", pid_col, pid_len, 'r')) 331 | items.append(item("start", startts_col, startts_len, 'r')) 332 | items.append(item("count", con_col, con_len, 'r')) 333 | items.append(item("connecttime", conts_col, conts_len, 'r')) 334 | items.append(item("disconnecttime", dists_col, dists_len, 'r')) 335 | 336 | 337 | # main loop 338 | while client.loop(0) == 0: 339 | key = stdscr.getch() 340 | if key == ord("q"): 341 | break 342 | if key == ord("?"): 343 | show_help() 344 | if key == ord("p"): 345 | client.publish("/clients/global/ping", "request") 346 | if key == ord("P"): 347 | n = get_client("Ping a Client") 348 | if n != 99999 and n < len(topiclist): 349 | clienttopic = topiclist[n] 350 | client.publish("/clients/" + clienttopic + "/ping", "request") 351 | if key == ord("i"): 352 | client.publish("/clients/global/identify", "request") 353 | if key == ord("I"): 354 | n = get_client("Identify a Client") 355 | if n != 99999 and n < len(topiclist): 356 | clienttopic = topiclist[n] 357 | client.publish("/clients/" + clienttopic + "/identify", "request") 358 | if key == ord("R"): 359 | n = get_client("Reset a Client") 360 | if n != 99999 and n < len(topiclist): 361 | clienttopic = topiclist[n] 362 | client.publish("/clients/" + clienttopic + "/command", "reload") 363 | if key == curses.KEY_DOWN: 364 | pos = pos + 1 365 | if pos > numclients: 366 | pos = numclients 367 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 368 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 369 | curses.doupdate() 370 | if key == curses.KEY_UP: 371 | pos = pos - 1 372 | if pos < 0: 373 | pos = 0 374 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 375 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 376 | curses.doupdate() 377 | if key == curses.KEY_NPAGE: 378 | pos = pos + page - 2 379 | if pos > numclients: 380 | pos = numclients 381 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 382 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 383 | curses.doupdate() 384 | if key == curses.KEY_PPAGE: 385 | pos = pos - page + 2 386 | if pos < 0: 387 | pos = 0 388 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 389 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 390 | curses.doupdate() 391 | if key == curses.KEY_LEFT: 392 | col = col - 1 393 | if col < 0: 394 | col = 0 395 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 396 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 397 | curses.doupdate() 398 | if key == curses.KEY_RIGHT: 399 | col = col + 1 400 | if col > 200: #3-curses.COLS: 401 | col = 200 #3-curses.COLS 402 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 403 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 404 | curses.doupdate() 405 | if key == curses.KEY_HOME: 406 | pos = 0 407 | col = 0 408 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 409 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 410 | curses.doupdate() 411 | if key == ord("b"): 412 | col = 0 413 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 414 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 415 | curses.doupdate() 416 | if key == ord("e"): 417 | col = 200 #3 - curses.COLS 418 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 419 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 420 | curses.doupdate() 421 | if key == curses.KEY_RESIZE: #Resizing the terminal screen crashes this program. 422 | l, c = getTerminalSize() #It seems to be something to do with calling the mosquitto library in the above while loop. 423 | if c > 50: #Redrawing belongs here if the problem gets solved. 424 | stdscr.addstr(0, curses.COLS-20, "Col: " + str(c) + " Lines: " + str(l)) 425 | l, c = getTerminalSize() 426 | if c > 120: 427 | stdscr.addstr(0,c - 27,"%s" % str(datetime.datetime.now()), curses.A_REVERSE) 428 | pass 429 | 430 | 431 | # clean up terminal settings 432 | curses.nocbreak() 433 | stdscr.keypad(0) 434 | curses.echo() 435 | curses.endwin() 436 | 437 | -------------------------------------------------------------------------------- /mprune: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # mprune - Prunes a topic tree of retained messages in an MQTT broker 4 | # 5 | # By Dennis Sell 6 | # 7 | # 8 | 9 | 10 | __author__ = "Dennis Sell" 11 | __copyright__ = "Copyright (C) Dennis Sell" 12 | 13 | 14 | import sys 15 | import signal 16 | import getopt 17 | import mosquitto 18 | 19 | 20 | topiclist = [] 21 | broker = "127.0.0.1" 22 | port = 1883 23 | topics = "/#" 24 | connected = False 25 | 26 | 27 | 28 | def usage(): 29 | print "usage: mprune [options]" #[hostname[:port]] [topics]" 30 | print "Delete retained topics from an MQTT broker." 31 | print 32 | print " -u --hostname HOSTNAME set hostname for broker (default localhost)" 33 | print " -p --port PORT set port for mqtt broker (default 1883)" 34 | print " -t --topic TOPIC set topic tree to delete" 35 | print " -h --help show this help information" 36 | print " -v --version show version information" 37 | print "By Dennis Sell -- 2013" 38 | 39 | 40 | def on_connect(mosq, userdata, rc): 41 | global connected 42 | connected = True 43 | print "Connected to ", broker 44 | 45 | def on_message(mosq, userdata, msg): 46 | if msg.topic not in topiclist and msg.retain: 47 | topiclist.append(msg.topic) 48 | print " " + msg.topic 49 | 50 | 51 | try: 52 | opts, args = getopt.getopt(sys.argv[1:], 'u:p:t:hv', ['hostname=', 'port', 'topic', 'help', 'version']) 53 | except getopt.GetoptError as err: 54 | print str(err) 55 | usage() 56 | sys.exit(2) 57 | 58 | 59 | for opt, arg in opts: 60 | if opt in ('-h', '--help'): 61 | usage() 62 | sys.exit(2) 63 | if opt in ('-v', '--version'): 64 | version() 65 | sys.exit(2) 66 | elif opt in ('-u', '--hostname'): 67 | broker = arg 68 | elif opt in ('-p', '--port'): 69 | port = int(arg) 70 | elif opt in ('-t', '--topic'): 71 | topics = arg 72 | else: 73 | usage() 74 | sys.exit(2) 75 | 76 | client = mosquitto.Mosquitto() 77 | client.on_message = on_message 78 | client.on_connect = on_connect 79 | client.connect(broker, port) 80 | client.subscribe(topics) 81 | 82 | def alarm_handler(signal, frame): 83 | global collect_state, flush_state 84 | collect_state = False 85 | flush_state = False 86 | 87 | 88 | print "Waiting for connection" 89 | while not connected: 90 | client.loop(0) 91 | 92 | 93 | signal.signal(signal.SIGALRM, alarm_handler) 94 | signal.alarm(5) # generate SIGALRM after 5 secs 95 | 96 | collect_state = True 97 | print "Collecting topics" 98 | while collect_state: 99 | client.loop(0) 100 | pass 101 | 102 | client.unsubscribe(topics) 103 | print "Erasing topics" 104 | for t in topiclist: 105 | print " Erasing topic: ", t 106 | client.publish(t, "", retain = True) 107 | client.loop(0) 108 | 109 | signal.alarm(2) # generate SIGALRM after 2 secs 110 | 111 | flush_state = True 112 | while flush_state: 113 | client.loop(0) 114 | 115 | 116 | -------------------------------------------------------------------------------- /mqttcore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | # vim tabstop=4 expandtab shiftwidth=4 softtabstop=4 4 | 5 | # 6 | # mqtt-core 7 | # 8 | # 9 | 10 | #TODO!!!!!!!!!!!!!!!HACKED VERSION FOR DASHBOARD!!!!!!!!!!!!!!!!!!! 11 | __author__ = "Dennis Sell" 12 | __copyright__ = "Copyright (C) Dennis Sell" 13 | 14 | 15 | import sys 16 | import os 17 | import mosquitto 18 | from mosquitto import error_string 19 | import socket 20 | import time 21 | import subprocess 22 | import logging 23 | import signal 24 | from config import Config 25 | from multiprocessing import Queue 26 | import datetime 27 | 28 | 29 | daemon_version = "na" 30 | 31 | 32 | COREVERSION = 0.7 33 | 34 | class MQTTClientCore: 35 | """ 36 | A generic MQTT client framework class 37 | 38 | """ 39 | def __init__(self, appname, clienttype, clean_session=True): 40 | self.running = True 41 | self.connectcount = 0 42 | self.starttime=datetime.datetime.now() 43 | self.connecttime=0 44 | self.disconnecttime=0 45 | self.mqtt_connected = False 46 | self.clienttype = clienttype 47 | self.clean_session = clean_session 48 | self.clientversion = "unknown" 49 | self.coreversion = COREVERSION 50 | self.q = Queue() 51 | homedir = os.path.expanduser("~") 52 | self.configfile = homedir + "/." + appname + '.conf' 53 | self.mqtttimeout = 60 # seconds 54 | 55 | if ('single' == self.clienttype): 56 | self.clientname = appname 57 | self.persist = True 58 | elif ('multi' == self.clienttype): 59 | self.persist = True 60 | self.clientname = appname + "[" + socket.gethostname() + "]" 61 | elif ('app' == self.clienttype): 62 | self.clientname = appname + "[" + socket.gethostname() + "_" +\ 63 | str(os.getpid()) + "]" 64 | self.persist = False 65 | else: # catchall 66 | self.clientname = appname 67 | self.clientbase = "/clients/" + self.clientname + "/" 68 | LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' 69 | 70 | #TODO need to deal with no config file existing!!! 71 | #read in configuration file 72 | f = self.configfile 73 | try: 74 | self.cfg = Config(f) 75 | except: 76 | try: 77 | self.cfg = Config('/etc/mqttclients/.' + appname + '.conf') 78 | except: 79 | try: 80 | self.cfg = Config('/etc/mqttclients/mqtt.conf') 81 | except: 82 | print "Config file not found." 83 | sys.exit(99) 84 | 85 | self.mqtthost = self.cfg.MQTT_HOST 86 | self.mqttport = self.cfg.MQTT_PORT 87 | self.mqtttimeout = 60 # get from config file TODO 88 | self.logfile = os.path.expanduser(self.cfg.LOGFILE) 89 | self.loglevel = self.cfg.LOGLEVEL 90 | try: 91 | self.ca_path = cfg.CA_PATH 92 | except: 93 | self.ca_path = None 94 | try: 95 | self.ssh_port = cfg.SSH_PORT 96 | self.ssh_host = cfg.SSH_HOST 97 | except: 98 | self.ssh_port = None 99 | self.ssh_host = None 100 | try: 101 | self.username = self.cfg.USERNAME 102 | except: 103 | self.username = None 104 | try: 105 | self.password = self.cfg.PASSWORD 106 | except: 107 | self.password = None 108 | 109 | logging.basicConfig(filename=self.logfile, level=self.loglevel, 110 | format=LOGFORMAT) 111 | 112 | #create an mqtt client 113 | self.mqttc = mosquitto.Mosquitto(self.clientname, self.clean_session, self.q) 114 | 115 | #trap kill signals including control-c 116 | signal.signal(signal.SIGTERM, self.cleanup) 117 | signal.signal(signal.SIGINT, self.cleanup) 118 | 119 | def getifip(ifn): 120 | # print ip 121 | import socket, fcntl, struct 122 | 123 | sck = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 124 | return socket.inet_ntoa(fcntl.ioctl(sck.fileno(),0x8915,struct.pack('256s', ifn[:15]))[20:24]) 125 | 126 | def identify(self): 127 | self.mqttc.publish(self.clientbase + "version", 128 | self.clientversion, qos=1, retain=self.persist) 129 | self.mqttc.publish(self.clientbase + "core-version", self.coreversion, qos=1, retain=self.persist) 130 | self.mqttc.publish(self.clientbase + "daemon-version", daemon_version(), qos=1, retain=self.persist) 131 | # p = subprocess.Popen("curl ifconfig.me/forwarded", shell=True, 132 | p = subprocess.Popen("ip -f inet addr show | tail -n 1 | cut -f 6 -d' ' | cut -f 1 -d'/'", shell=True, 133 | stdout=subprocess.PIPE) 134 | ip = p.stdout.readline() 135 | self.mqttc.publish(self.clientbase + "locip", ip.strip('\n'), qos=1, retain=self.persist) 136 | p = subprocess.Popen("curl -s ifconfig.me/ip", shell=True, 137 | stdout=subprocess.PIPE) 138 | extip = p.stdout.readline() 139 | self.mqttc.publish(self.clientbase + "extip", extip.strip('\n'), qos=1, retain=self.persist) 140 | self.mqttc.publish(self.clientbase + "pid", os.getpid(), qos=1, retain=self.persist) 141 | self.mqttc.publish(self.clientbase + "status", "online", qos=1, retain=self.persist) 142 | self.mqttc.publish(self.clientbase + "class", self.clienttype, qos=1, retain=self.persist) 143 | self.mqttc.publish(self.clientbase + "start", str(self.starttime), qos=1, retain=self.persist) 144 | self.mqttc.publish(self.clientbase + "disconnecttime", str(self.disconnecttime), qos=1, retain=self.persist) 145 | self.mqttc.publish(self.clientbase + "connecttime", str(self.connecttime), qos=1, retain=self.persist) 146 | self.mqttc.publish(self.clientbase + "count", self.connectcount, qos=1, retain=self.persist) 147 | 148 | #define what happens after connection 149 | def on_connect(self, mself, obj, rc): 150 | self.mqtt_connected = True 151 | self.connectcount = self.connectcount+1 152 | self.connecttime=datetime.datetime.now() 153 | print "MQTT Connected" 154 | logging.info("MQTT connected") 155 | self.mqttc.subscribe(self.clientbase + "ping", qos=2) 156 | self.mqttc.subscribe("/clients/global/#", qos=2) 157 | self.identify() 158 | 159 | def on_disconnect( self, mself, obj, rc ): 160 | self.disconnecttime=datetime.datetime.now() 161 | logging.info("MQTT disconnected: " + error_string(rc)) 162 | print "MQTT Disconnected" 163 | 164 | #On recipt of a message create a pynotification and show it 165 | def on_message( self, mself, obj, msg): 166 | if ((( msg.topic == self.clientbase + "ping" ) and 167 | ( msg.payload == "request" )) or 168 | (( msg.topic == "/clients/global/ping" ) and 169 | ( msg.payload == "request" ))): 170 | self.mqttc.publish(self.clientbase + "ping", "response", qos=1, 171 | retain=0) 172 | if (( msg.topic == "/clients/global/identify" ) and 173 | ( msg.payload == "request" )): 174 | self.identify() 175 | 176 | def on_log(self, mself, obj, level, buffer): 177 | logging.info(buffer) 178 | #TODO add level filtering here 179 | 180 | def mqtt_connect(self): 181 | if ( True != self.mqtt_connected ): 182 | print "Attempting connection..." 183 | logging.info("Attempting connection.") 184 | if(self.ssh_port != None): 185 | logging.info("Building SSH tunnel.") 186 | print "Using ssh for security" 187 | self.sshpid = subprocess.Popen("ssh -f -n -N -L 127.0.0.1:%d:localhost:%d user@%s" 188 | % (self.ssh_port, self.mqtt_port, self.ssh_host), 189 | shell=True, close_fds=True) 190 | self.mqtt_host = "localhost" 191 | self.mqtt_port = self.ssh_port 192 | else: 193 | self.sshpid = None 194 | if(self.ca_path != None): 195 | logging.info("Assigning certificate for security.") 196 | print "Using CA for security" 197 | self.mqttc.tls_set(self.ca_path) 198 | if self.username != None: 199 | if self.password != None: 200 | logging.info("Using username for login") 201 | print "Logging in as " + self.username + " with password." 202 | self.mqttc.username_pw_set(self.username, self.password) 203 | else: 204 | logging.info("Using password for login") 205 | print "Logging in as " + self.username 206 | self.mqttc.username_pw_set(self.username) 207 | self.mqttc.will_set(self.clientbase + "/status", "disconnected", qos=1, retain=self.persist) 208 | 209 | #define the mqtt callbacks 210 | self.mqttc.on_message = self.on_message 211 | self.mqttc.on_connect = self.on_connect 212 | self.mqttc.on_disconnect = self.on_disconnect 213 | self.mqttc.on_log = self.on_log 214 | 215 | #connect 216 | self.mqttc.connect_async(self.mqtthost, self.mqttport, 217 | self.mqtttimeout) 218 | 219 | def mqtt_disconnect(self): 220 | if ( self.mqtt_connected ): 221 | self.mqtt_connected = False 222 | logging.info("MQTT disconnecting") 223 | print "MQTT Disconnecting" 224 | self.mqttc.publish ( self.clientbase + "status" , "offline", qos=1, retain=self.persist ) 225 | self.mqttc.disconnect() 226 | try: 227 | logging.info("Destroying SSH tunnel.") 228 | print "Destroying SSH tunnel." 229 | os.kill(self.sshpid, 15) # 15 = SIGTERM 230 | except: 231 | print "PID invalid" 232 | 233 | def cleanup(self, signum, frame): 234 | self.running = False 235 | self.mqtt_disconnect() 236 | sys.exit(signum) 237 | 238 | def main_loop(self): 239 | self.mqtt_connect() 240 | self.mqttc.loop_forever() 241 | 242 | 243 | def main(daemon): 244 | if len(sys.argv) == 2: 245 | if 'start' == sys.argv[1]: 246 | daemon.start() 247 | elif 'stop' == sys.argv[1]: 248 | daemon.stop() 249 | elif 'restart' == sys.argv[1]: 250 | daemon.restart() 251 | elif 'run' == sys.argv[1]: 252 | daemon.run() 253 | else: 254 | print "Unknown command" 255 | sys.exit(2) 256 | sys.exit(0) 257 | else: 258 | print "usage: %s start|stop|restart" % sys.argv[0] 259 | sys.exit(2) 260 | -------------------------------------------------------------------------------- /mwatch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # mwatch - Watches topics change from an MQTT broker 4 | # 5 | # By Dennis Sell 6 | # 7 | # Inspired by subcurses written by Andrew Elwell 8 | 9 | 10 | 11 | 12 | __author__ = "Dennis Sell" 13 | __copyright__ = "Copyright (C) Dennis Sell" 14 | 15 | 16 | #BUGS 17 | # resizing terminal crashes. Possibly some curses/mosquitto interaction. 18 | 19 | 20 | import os 21 | import sys 22 | import getopt 23 | import mosquitto 24 | import curses 25 | from curses.ascii import isprint 26 | import time 27 | import datetime 28 | import mqttcore 29 | 30 | 31 | version_number = 0.7 32 | topiclist = [] 33 | count = [] 34 | numtopics = 0 35 | rxcount = 0 36 | pos = 0 37 | col = 0 38 | topic_col = 0 39 | topic_size = 65 40 | retained_col = topic_size 41 | qos_col = topic_size 42 | count_col = topic_size 43 | timestamp_col = topic_size 44 | message_col = topic_size 45 | 46 | 47 | def usage(): 48 | print "usage: mwatch [options]" #[hostname[:port]] [topics]" 49 | print "Watch topics change from an MQTT broker." 50 | print 51 | print " -u --hostname HOSTNAME set hostname for broker (default localhost)" 52 | print " -p --port PORT set port for mqtt broker (default 1883)" 53 | print " -t --topics TOPICS set topics to watch (default /#)" 54 | print " -q --qos show QOS as recieved" 55 | print " -r --retained show retained status" 56 | print " -c --count show count of receptions" 57 | print " -s --timestamp show timestamp of message reception" 58 | print " -a --all show all data fields" 59 | print " -x --hex show message in hex" 60 | print " -h --help show this help information" 61 | print " -v --version show version information" 62 | print "By Dennis Sell -- 2013" 63 | 64 | 65 | def version(): 66 | print "version: " + str(version_number) 67 | 68 | 69 | def show_help(): 70 | help_cols = curses.COLS/2 71 | help_lines = curses.LINES/2 72 | help_win = curses.newwin(24, 50, help_lines-12, help_cols-25) 73 | help_win.box() 74 | help_win.addstr(1, 1, "Key Commands", curses.A_UNDERLINE) 75 | help_win.addstr(3, 1, "[Home] - Jump to top left") 76 | help_win.addstr(4, 1, "[END] - Jump to bottom of list") 77 | help_win.addstr(5, 1, "b - Jump to begining of line") 78 | help_win.addstr(6, 1, "e - Jump to end of line") 79 | help_win.addstr(7, 1, "[Right Arrow] - Scroll to right") 80 | help_win.addstr(8, 1, "[Left Arrow] - Scroll to left") 81 | help_win.addstr(9, 1, "[Up Arrow] - Scroll up") 82 | help_win.addstr(10, 1, "[Down Arrow] - Scroll down") 83 | help_win.addstr(11, 1, "[Page Up] - Page up") 84 | help_win.addstr(12, 1, "[Page Down] - Page down") 85 | help_win.addstr(14, 1, "Retained flag == True implies the message was") 86 | help_win.addstr(15, 1, "sent before mwatch connected to the broker. ") 87 | help_win.addstr(17, 1, "q - To exit help.") 88 | help_win.noutrefresh() 89 | curses.doupdate() 90 | while True: 91 | key = stdscr.getch() 92 | if key == ord("q"): 93 | break 94 | help_win.erase() 95 | del help_win 96 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 97 | header_txt.refresh(0, 0, 2, 1, 2, curses.COLS-3) 98 | stdscr.nooutrefresh() 99 | curses.doupdate() 100 | 101 | 102 | def on_connect(mosq, userdata, rc): 103 | stdscr.addstr(curses.LINES-1,0,"Connected to %s" % broker) 104 | stdscr.addstr(0,15,"Sunscribed to %s" % topics, curses.A_REVERSE) 105 | stdscr.noutrefresh() 106 | curses.doupdate() 107 | 108 | 109 | def isprintable(v): 110 | ret = True 111 | for char in v: 112 | if not curses.ascii.isprint(char): 113 | ret = False 114 | return ret 115 | 116 | 117 | def printable(input): 118 | return ''.join(char for char in input if isprint(char)) 119 | 120 | 121 | def on_message(mosq, userdata, msg): 122 | global numtopics 123 | global rxcount 124 | global retained_flag 125 | global timestamp_flag 126 | 127 | rxcount = rxcount + 1 128 | stdscr.addstr(0,80,"Msg count %s" % rxcount, curses.A_REVERSE) 129 | if msg.topic not in topiclist: 130 | topiclist.append(msg.topic) 131 | numtopics = numtopics + 1 132 | count.append(0) 133 | stdscr.addstr(0,60,"Topic count %s" % numtopics, curses.A_REVERSE) 134 | offset = topiclist.index(msg.topic) 135 | count[offset] = count[offset] + 1 136 | stats_txt.move(offset, message_col) 137 | stats_txt.clrtoeol() 138 | stats_txt.addstr(offset, topic_col, msg.topic) 139 | if isprintable(msg.payload) and not hex_flag: 140 | stats_txt.addstr(offset, message_col, msg.payload[0:1900]) 141 | else: 142 | v = " ".join([hex(ord(a)) for a in msg.payload]) #Does individual bytes. 143 | stats_txt.addstr(offset, message_col, "#### NON-ALPHANUMERIC #### --> 0x" + msg.payload.encode('hex')[0:1900]) 144 | if msg.retain: 145 | retained_value = "True " 146 | else: 147 | retained_value = "False" 148 | if retained_flag: 149 | stats_txt.addstr(offset, retained_col, retained_value) 150 | if qos_flag: 151 | stats_txt.addstr(offset, qos_col, str(msg.qos)) 152 | if count_flag: 153 | stats_txt.addstr(offset, count_col, str(count[offset])) 154 | if timestamp_flag: 155 | stats_txt.addstr(offset, timestamp_col, str(datetime.datetime.now())) 156 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 157 | curses.doupdate() 158 | 159 | 160 | def getTerminalSize(): 161 | """ 162 | returns (lines:int, cols:int) 163 | """ 164 | import os, struct 165 | def ioctl_GWINSZ(fd): 166 | import fcntl, termios 167 | return struct.unpack("hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, "1234")) 168 | # try stdin, stdout, stderr 169 | for fd in (0, 1, 2): 170 | try: 171 | return ioctl_GWINSZ(fd) 172 | except: 173 | pass 174 | # try os.ctermid() 175 | try: 176 | fd = os.open(os.ctermid(), os.O_RDONLY) 177 | try: 178 | return ioctl_GWINSZ(fd) 179 | finally: 180 | os.close(fd) 181 | except: 182 | pass 183 | # try `stty size` 184 | try: 185 | return tuple(int(x) for x in os.popen("stty size", "r").read().split()) 186 | except: 187 | pass 188 | # try environment variables 189 | try: 190 | return tuple(int(os.getenv(var)) for var in ("LINES", "COLUMNS")) 191 | except: 192 | pass 193 | # i give up. return default. 194 | return (25, 80) 195 | 196 | 197 | try: 198 | opts, args = getopt.getopt(sys.argv[1:], 'u:p:t:rqcsaxhv', ['hostname=', 'port=', 'topics=', 'retained', 'qos', 'count', 'timestamp', 'all', 'hex', 'help', 'version']) 199 | except getopt.GetoptError as err: 200 | print str(err) 201 | usage() 202 | sys.exit(2) 203 | 204 | 205 | # defaults 206 | broker = "127.0.0.1" 207 | port = 1883 208 | topics = "/#" 209 | retained_flag = False 210 | qos_flag = False 211 | count_flag = False 212 | timestamp_flag = False 213 | hex_flag = False 214 | 215 | for opt, arg in opts: 216 | if opt in ('-h', '--help'): 217 | usage() 218 | sys.exit(2) 219 | if opt in ('-v', '--version'): 220 | version() 221 | sys.exit(2) 222 | elif opt in ('-u', '--hostname'): 223 | broker = arg 224 | elif opt in ('-p', '--port'): 225 | port = int(arg) 226 | elif opt in ('-t', '--topics'): 227 | topics = arg 228 | elif opt in ('-r', '--retained'): 229 | retained_flag = True 230 | elif opt in ('-q', '--qos'): 231 | qos_flag = True 232 | elif opt in ('-c', '--count'): 233 | count_flag = True 234 | elif opt in ('-s', '--timestamp'): 235 | timestamp_flag = True 236 | elif opt in ('-a', '--all'): 237 | retained_flag = True 238 | qos_flag = True 239 | count_flag = True 240 | timestamp_flag = True 241 | elif opt in ('-x', '--hex'): 242 | hex_flag = True 243 | else: 244 | usage() 245 | sys.exit(2) 246 | 247 | 248 | # minitialize mosquitto, connect, and subscribe 249 | client = mosquitto.Mosquitto() 250 | client.on_message = on_message 251 | client.on_connect = on_connect 252 | client.connect(broker, port) 253 | if qos_flag: 254 | qos = 2 255 | else: 256 | qos = 0 257 | client.subscribe(topics, qos=qos) 258 | 259 | # initialize curses 260 | stdscr = curses.initscr() 261 | if curses.COLS < 95: 262 | curses.endwin() 263 | print "mwatch requires a minimum terminal width of 95 columns." 264 | sys.exit(2) 265 | if curses.LINES < 7: 266 | curses.endwin() 267 | print "mwatch requires a minimum terminal heigth of 7 rows." 268 | sys.exit(2) 269 | stdscr.nodelay(1) 270 | curses.curs_set(0) 271 | if curses.has_colors(): 272 | curses.start_color() 273 | curses.cbreak() 274 | curses.noecho() 275 | stdscr.keypad(1) 276 | 277 | 278 | # draw basic screen 279 | stdscr.addstr("MQTT watch ", curses.A_REVERSE) 280 | stdscr.chgat(-1,curses.A_REVERSE) 281 | stdscr.addstr(curses.LINES-1,curses.COLS-57,"page-up, page-dn, cursor arrows, ? for help, q to Quit") 282 | page = curses.LINES - 3 283 | stats = curses.newwin(curses.LINES-2,curses.COLS,1,0) 284 | stats.box() 285 | header_txt = curses.newpad(1,2000) 286 | stats_txt = curses.newpad(2000,2000) 287 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 288 | stdscr.noutrefresh() 289 | stats.noutrefresh() 290 | 291 | 292 | header_txt.addstr(0, topic_col+1, "TOPIC", curses.A_UNDERLINE) 293 | if retained_flag: 294 | qos_col = qos_col + 10 295 | timestamp_col = timestamp_col + 10 296 | count_col = count_col + 10 297 | message_col = message_col + 10 298 | if curses.COLS > retained_col + 2: 299 | header_txt.addstr(0, retained_col+1, "RETAINED", curses.A_UNDERLINE) 300 | if qos_flag: 301 | timestamp_col = timestamp_col + 5 302 | count_col = count_col + 5 303 | message_col = message_col + 5 304 | header_txt.addstr(0, qos_col+1, "QOS", curses.A_UNDERLINE) 305 | if count_flag: 306 | timestamp_col = timestamp_col + 7 307 | message_col = message_col + 7 308 | header_txt.addstr(0, count_col+1, "COUNT", curses.A_UNDERLINE) 309 | if timestamp_flag: 310 | message_col = message_col + 28 311 | header_txt.addstr(0, timestamp_col+1, "RECEIVED TIMESTAMP", curses.A_UNDERLINE) 312 | if curses.COLS > message_col + 10: 313 | header_txt.addstr(0, message_col+1, "MESSAGE", curses.A_UNDERLINE) 314 | header_txt.refresh(0, 0, 2, 1, 2, curses.COLS-3) 315 | stats.noutrefresh() 316 | curses.doupdate() 317 | 318 | # main loop 319 | while client.loop(0) == 0: 320 | key = stdscr.getch() 321 | if key == -1: 322 | pass 323 | elif key == ord("q"): 324 | break 325 | elif key == ord("?"): 326 | show_help() 327 | elif key == curses.KEY_DOWN: 328 | pos = pos + 1 329 | if pos > numtopics: 330 | pos = numtopics 331 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 332 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 333 | curses.doupdate() 334 | elif key == curses.KEY_UP: 335 | pos = pos - 1 336 | if pos < 0: 337 | pos = 0 338 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 339 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 340 | curses.doupdate() 341 | elif key == curses.KEY_NPAGE: 342 | pos = pos + page - 2 343 | if pos > numtopics: 344 | pos = numtopics 345 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 346 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 347 | curses.doupdate() 348 | elif key == curses.KEY_PPAGE: 349 | pos = pos - page + 2 350 | if pos < 0: 351 | pos = 0 352 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 353 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 354 | curses.doupdate() 355 | elif key == curses.KEY_LEFT: 356 | col = col - 1 357 | if col < 0: 358 | col = 0 359 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 360 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 361 | curses.doupdate() 362 | elif key == curses.KEY_RIGHT: 363 | col = col + 1 364 | if col > 2003-curses.COLS: 365 | col = 2003-curses.COLS 366 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 367 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 368 | curses.doupdate() 369 | elif key == curses.KEY_HOME: 370 | pos = 0 371 | col = 0 372 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 373 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 374 | curses.doupdate() 375 | elif key == curses.KEY_END: 376 | pos = numtopics - page + 4 377 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 378 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 379 | curses.doupdate() 380 | elif key == ord("b"): 381 | col = 0 382 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 383 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 384 | curses.doupdate() 385 | elif key == ord("e"): 386 | col = 2003 - curses.COLS 387 | stats_txt.refresh(pos, col, 3,2, page,curses.COLS-3) 388 | header_txt.refresh(0, col, 2, 1, 2, curses.COLS-3) 389 | curses.doupdate() 390 | elif key == curses.KEY_RESIZE: #Resizing the terminal screen crashes this program. 391 | l, c = getTerminalSize() #It seems to be something to do with calling the mosquitto library in the above while loop. 392 | if c > 50: #Redrawing belongs here if the problem gets solved. 393 | stdscr.addstr(0, curses.COLS-20, "Col: " + str(c) + " Lines: " + str(l)) 394 | l, c = getTerminalSize() 395 | if c > 120: 396 | stdscr.addstr(0,c - 27,"%s" % str(datetime.datetime.now()), curses.A_REVERSE) 397 | pass 398 | 399 | 400 | # clean up terminal settings 401 | curses.nocbreak() 402 | stdscr.keypad(0) 403 | curses.echo() 404 | curses.endwin() 405 | 406 | 407 | --------------------------------------------------------------------------------