Browse Source

split parts of ffpb.by into ffpb_{netstatus,nodeinfo}.py

ALFRED data is now fetched via ffpb's get_alfred_data().

ffpb_netstatus contains functions like !status, !highscore and !rollout-status.

ffpb_nodeinfo contains functions like !info, !link, !mesh, etc.
Helge Jung 9 years ago
parent
commit
4eb145c13f
3 changed files with 366 additions and 316 deletions
  1. 71 316
      modules/ffpb.py
  2. 152 0
      modules/ffpb_netstatus.py
  3. 143 0
      modules/ffpb_nodeinfo.py

+ 71 - 316
modules/ffpb.py

@@ -23,13 +23,10 @@ import threading
 
 msgserver = None
 peers_repo = None
-highscores = None
 
 nodeaccess = None
 
 alfred_method = None
-alfred_data = None
-alfred_update = datetime.datetime(1970,1,1,23,42)
 
 ffpb_resolver = dns.resolver.Resolver ()
 ffpb_resolver.nameservers = ['10.132.254.53']
@@ -75,20 +72,11 @@ class ThreadingTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
 	pass
 
 def setup(bot):
-	global msgserver, peers_repo, alfred_method, highscores, nodeaccess
+	global msgserver, peers_repo, alfred_method, nodeaccess
 
 	# signal begin of setup routine
 	bot.memory['ffpb_in_setup'] = True
 
-	# load highscores from disk
-	highscores = shelve.open('highscoredata', writeback=True)
-	if not 'nodes' in highscores:
-		highscores['nodes'] = 0
-		highscores['nodes_ts'] = time.time()
-	if not 'clients' in highscores:
-		highscores['clients'] = 0
-		highscores['clients_ts'] = time.time()
-
 	# load list of seen nodes from disk
 	seen_nodes = shelve.open('nodes.seen', writeback=True)
 	bot.memory['seen_nodes'] = seen_nodes
@@ -124,19 +112,17 @@ def setup(bot):
 
 	# initially fetch ALFRED data
 	alfred_method = bot.config.ffpb.alfred_method
+	if not 'alfred_data' in bot.memory:
+		bot.memory['alfred_data'] = {}
+	if not 'alfred_update' in bot.memory:
+		bot.memory['alfred_update'] = datetime.datetime(1970,1,1,23,42)
 	ffpb_updatealfred(bot)
 
 	# signal end of setup routine
 	bot.memory['ffpb_in_setup'] = False
 
 def shutdown(bot):
-	global msgserver, highscores, nodeaccess
-
-	# store highscores
-	if not highscores is None:
-		highscores.sync()
-		highscores.close()
-		highscores = None
+	global msgserver, nodeaccess
 
 	# store node acl
 	if not nodeaccess is None:
@@ -337,7 +323,7 @@ def ffpb_ensurenodeid(nodedata):
 
 	return result
 
-def ffpb_findnode(name):
+def ffpb_findnode(name, alfred_data = None):
 	"""helper: try to identify the node the user meant by the given name"""
 
 	# no name, no node
@@ -348,41 +334,42 @@ def ffpb_findnode(name):
 
 	names = {}
 
-	# try to match MAC
-	m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
-	if (not m is None):
-		mac = m.group(0).lower()
-		if mac in alfred_data:
-			return ffpb_ensurenodeid(alfred_data[mac])
+	if not alfred_data is None:
+		# try to match MAC
+		m = re.search("^([0-9a-fA-F][0-9a-fA-F]:){5}[0-9a-fA-F][0-9a-fA-F]$", name)
+		if (not m is None):
+			mac = m.group(0).lower()
+			if mac in alfred_data:
+				return ffpb_ensurenodeid(alfred_data[mac])
+
+			# try to find alias MAC in ALFRED data
+			for nodeid in alfred_data:
+				node = alfred_data[nodeid]
+				if "network" in node:
+					if "mac" in node["network"] and node["network"]["mac"].lower() == mac:
+						return ffpb_ensurenodeid(node)
+					if "mesh_interfaces" in node["network"]:
+						for mim in node["network"]["mesh_interfaces"]:
+							if mim.lower() == mac:
+								return ffpb_ensurenodeid(node)
+
+			nodeid = mac.replace(':','').lower()
+			return {
+				'nodeid': nodeid,
+				'hostname': '?-' + nodeid,
+				'network': { 'addresses': [ mac2ipv6(mac, 'fdca:ffee:ff12:132:') ], 'mac': mac, },
+				'hardware': { 'model': 'derived-from-mac' },
+			}
 
-		# try to find alias MAC in ALFRED data
+		# look through the ALFRED peers
 		for nodeid in alfred_data:
 			node = alfred_data[nodeid]
-			if "network" in node:
-				if "mac" in node["network"] and node["network"]["mac"].lower() == mac:
-					return ffpb_ensurenodeid(node)
-				if "mesh_interfaces" in node["network"]:
-					for mim in node["network"]["mesh_interfaces"]:
-						if mim.lower() == mac:
-							return ffpb_ensurenodeid(node)
-
-		nodeid = mac.replace(':','').lower()
-		return {
-			'nodeid': nodeid,
-			'hostname': '?-' + nodeid,
-			'network': { 'addresses': [ mac2ipv6(mac, 'fdca:ffee:ff12:132:') ], 'mac': mac, },
-			'hardware': { 'model': 'derived-from-mac' },
-		}
-
-	# look through the ALFRED peers
-	for nodeid in alfred_data:
-		node = alfred_data[nodeid]
-		if 'hostname' in node:
-			h = node['hostname']
-			if h.lower() == name.lower():
-				return node
-			else:
-				names[h] = nodeid
+			if 'hostname' in node:
+				h = node['hostname']
+				if h.lower() == name.lower():
+					return node
+				else:
+					names[h] = nodeid
 
 	# not found in ALFRED data -> try peers_repo
 	if not peers_repo is None:
@@ -412,11 +399,12 @@ def ffpb_findnode(name):
 			}
 
 	# do a similar name lookup in the ALFRED data
-	possibilities = difflib.get_close_matches(name, [ x for x in names ], cutoff=0.75)
-	print('findnode: Fuzzy matching \'{0}\' got {1} entries: {2}'.format(name, len(possibilities), ', '.join(possibilities)))
-	if len(possibilities) == 1:
-		# if we got exactly one candidate that might be it
-		return ffpb_ensurenodeid(alfred_data[names[possibilities[0]]])
+	if not alfred_data is None:
+		possibilities = difflib.get_close_matches(name, [ x for x in names ], cutoff=0.75)
+		print('findnode: Fuzzy matching \'{0}\' got {1} entries: {2}'.format(name, len(possibilities), ', '.join(possibilities)))
+		if len(possibilities) == 1:
+			# if we got exactly one candidate that might be it
+			return ffpb_ensurenodeid(alfred_data[names[possibilities[0]]])
 
 	# none of the above was able to identify the requested node
 	return None
@@ -428,15 +416,13 @@ def ffpb_findnode_from_botparam(bot, name, ensure_recent_alfreddata = True):
 		if not bot is None: bot.reply("Grün.")
 		return None
 
+	alfred_data = get_alfred_data(bot, ensure_recent_alfreddata)
 	if ensure_recent_alfreddata and alfred_data is None:
-		if not bot is None: bot.say("Informationen sind ausverkauft, kommen erst morgen wieder rein.")
-		return None
-
-	if ensure_recent_alfreddata and ffpb_alfred_data_outdated():
-		if not bot is None: bot.say("Ich habe gerade keine aktuellen Informationen, daher sage ich mal lieber nichts zu '" + name + "'.")
+		if not bot is None:
+			bot.say("Informationen sind ausverkauft bzw. veraltet, daher sage ich mal lieber nichts zu '" + name + "'.")
 		return None
 
-	node = ffpb_findnode(name)
+	node = ffpb_findnode(name, alfred_data)
 	if node is None:
 		if not bot is None: bot.say("Kein Plan wer oder was mit '" + name + "' gemeint ist :(")
 
@@ -454,7 +440,6 @@ def mac2ipv6(mac, prefix=None):
 @willie.module.interval(30)
 def ffpb_updatealfred(bot):
 	"""Aktualisiere ALFRED-Daten"""
-	global alfred_data, alfred_update
 
 	if alfred_method is None or alfred_method == "None":
 		return
@@ -478,12 +463,14 @@ def ffpb_updatealfred(bot):
 	try:
 		alfred_data = json.load(rawdata)
 		#print("Fetched new ALFRED data:", len(alfred_data), "entries")
-		alfred_update = updated
 
 	except ValueError as e:
 		print("Failed to parse ALFRED data: " + str(e))
 		return
 
+	bot.memory['alfred_data'] = alfred_data
+	bot.memory['alfred_update'] = updated
+
 	seen_nodes = bot.memory['seen_nodes'] if 'seen_nodes' in bot.memory else None
 	if not seen_nodes is None:
 		new = []
@@ -518,13 +505,23 @@ def ffpb_updatealfred(bot):
 			action_target = bot.config.ffpb.msg_target
 			bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
 
-def ffpb_alfred_data_outdated():
-	"""Check if the stored alfred_data has been updated more than 5 minutes ago."""
+def get_alfred_data(bot, ensure_not_outdated=True):
+	"""Retrieves the stored alfred_data and optionally checks that it has been updated no more than 5 minutes ago."""
 
-	timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
-	is_outdated = timeout > alfred_update
-	#print("ALFRED outdated? {0} (timeout={1} vs. lastupdate={2})".format(is_outdated, timeout, alfred_update))
-	return is_outdated
+	alfred_data = bot.memory['alfred_data'] if 'alfred_data' in bot.memory else None
+	alfred_update = bot.memory['alfred_update'] if 'alfred_update' in bot.memory else 0
+
+	if alfred_data is None:
+		return None
+
+	if ensure_not_outdated:
+		timeout = datetime.datetime.now() - datetime.timedelta(minutes=5)
+		is_outdated = timeout > alfred_update
+		#print("ALFRED outdated? {0} (timeout={1} vs. lastupdate={2})".format(is_outdated, timeout, alfred_update))
+		if is_outdated:
+			return None
+
+	return alfred_data 
 
 def ffpb_get_batcave_nodefield(nodeid, field):
 	"""Query the given field for the given nodeid from the BATCAVE."""
@@ -552,141 +549,11 @@ def ffpb_debug_alfred(bot, trigger):
 	if not playitsafe(bot, trigger, botadmin=True, via_privmsg=True):
 		return
 
+	alfred_data = get_alfred_data(bot)
 	if alfred_data is None:
 		bot.say("Keine ALFRED-Daten vorhanden.")
 	else:
-		bot.say("ALFRED Daten: count={0} lastupdate={1}".format(len(alfred_data), alfred_update))
-
-@willie.module.commands('alfred-data')
-def ffpb_peerdata(bot, trigger):
-	"""Show ALFRED data of the given node."""
-
-	# identify node or bail out
-	target_name = trigger.group(2)
-	node = ffpb_findnode_from_botparam(bot, target_name)
-	if node is None: return
-
-	# query must be a PM or as OP in the channel
-	if not playitsafe(bot, trigger, need_op=True, node=node):
-		# the check function already gives a bot reply, just exit here
-		return
-
-	# reply each key in the node's data
-	for key in node:
-		if key in [ 'hostname' ]: continue
-		bot.say("{0}.{1} = {2}".format(node['hostname'], key, str(node[key])))
-
-@willie.module.commands('info')
-def ffpb_peerinfo(bot, trigger):
-	"""Show information of the given node."""
-
-	# identify node or bail out
-	target_name = trigger.group(2)
-	node = ffpb_findnode_from_botparam(bot, target_name)
-	if node is None: return
-
-	# read node information
-	info_mac = node['network']['mac'] if 'network' in node and 'mac' in node['network'] else '??:??:??:??:??:??'
-	info_id = node['node_id'] if 'node_id' in node else info_mac.replace(':','')
-	info_name = node['hostname'] if 'hostname' in node else '?-' + info_id
-
-	info_hw = ""
-	if "hardware" in node:
-		if "model" in node["hardware"]:
-			model = node["hardware"]["model"]
-			info_hw = " model='" + model + "'"
-
-	info_fw = ""
-	info_update = ""
-	if "software" in node:
-		if "firmware" in node["software"]:
-			fwinfo = str(node["software"]["firmware"]["release"]) if "release" in node["software"]["firmware"] else "unknown"
-			info_fw = " firmware=" + fwinfo
-
-		if "autoupdater" in node["software"]:
-			autoupdater = node["software"]["autoupdater"]["branch"] if node["software"]["autoupdater"]["enabled"] else "off"
-			info_update = " (autoupdater="+autoupdater+")"
-
-	info_uptime = ""
-	u = -1
-	if "statistics" in node and "uptime" in node["statistics"]:
-		u = int(float(node["statistics"]["uptime"]))
-	elif 'uptime' in node:
-		u = int(float(node['uptime']))
-
-	if u > 0:
-		d, r1 = divmod(u, 86400)
-		h, r2 = divmod(r1, 3600)
-		m, s = divmod(r2, 60)
-		if d > 0:
-			info_uptime = ' up {0}d {1}h'.format(d,h)
-		elif h > 0:
-			info_uptime = ' up {0}h {1}m'.format(h,m)
-		else:
-			info_uptime = ' up {0}m'.format(m)
-
-	info_clients = ""
-	clientcount = ffpb_get_batcave_nodefield(info_id, 'clientcount')
-	if not clientcount is None:
-		clientcount = int(clientcount)
-		info_clients = ' clients={0}'.format(clientcount)
-
-	bot.say('[{1}]{2}{3}{4}{5}{6}'.format(info_mac, info_name, info_hw, info_fw, info_update, info_uptime, info_clients))
-
-@willie.module.commands('uptime')
-def ffpb_peeruptime(bot, trigger):
-	"""Display the uptime of the given node."""
-
-	# identify node or bail out
-	target_name = trigger.group(2)
-	node = ffpb_findnode_from_botparam(bot, target_name)
-	if node is None: return
-
-	# get name and raw uptime from node
-	info_name = node["hostname"]
-	info_uptime = ''
-	u_raw = None
-	if 'statistics' in node and 'uptime' in node['statistics']:
-		u_raw = node['statistics']['uptime']
-	elif 'uptime' in node:
-		u_raw = node['uptime']
-
-	# pretty print uptime
-	if not u_raw is None:
-		u = int(float(u_raw))
-		d, r1 = divmod(u, 86400)
-		h, r2 = divmod(r1, 3600)
-		m, s = divmod(r2, 60)
-		if d > 0:
-			info_uptime += '{0}d '.format(d)
-		if h > 0:
-			info_uptime += '{0}h '.format(h)
-		info_uptime += '{0}m'.format(m)
-		info_uptime += ' # raw: \'{0}\''.format(u_raw)
-	else:
-		info_uptime += '?'
-
-	# reply to user
-	bot.say('uptime(\'{0}\') = {1}'.format(info_name, info_uptime))
-
-@willie.module.commands('link')
-def ffpb_peerlink(bot, trigger):
-	"""Display MAC and link to statuspage for the given node."""
-
-	# identify node or bail out
-	target_name = trigger.group(2)
-	node = ffpb_findnode_from_botparam(bot, target_name)
-	if node is None: return
-
-	# get node's MAC
-	info_mac = node["network"]["mac"]
-	info_name = node["hostname"]
-
-	# get node's v6 address in the mesh (derived from MAC address)
-	info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
-
-	# reply to user
-	bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
+		bot.say("ALFRED Daten: count={0} lastupdate={1}".format(len(alfred_data), bot.memory['alfred_update']))
 
 @willie.module.interval(60)
 def ffpb_updatepeers(bot):
@@ -768,43 +635,6 @@ def ffpb_fetch_stats(bot, url, memoryid):
 
 	return (nodes_active, nodes_total, clients_count)
 
-@willie.module.interval(15)
-def ffpb_get_stats(bot):
-	"""Fetch current statistics, if the highscore changes signal this."""
-
-	(nodes_active, nodes_total, clients_count) = ffpb_fetch_stats(bot, 'http://map.paderborn.freifunk.net/nodes.json', 'ffpb_stats')
-
-	highscore_changed = False
-	if nodes_active > highscores['nodes']:
-		highscores['nodes'] = nodes_active
-		highscores['nodes_ts'] = time.time()
-		highscore_changed = True
-
-	if clients_count > highscores['clients']:
-		highscores['clients'] = clients_count
-		highscores['clients_ts'] = time.time()
-		highscore_changed = True
-
-	if highscore_changed:
-		print('HIGHSCORE changed: {0} nodes ({1}), {2} clients ({3})'.format(highscores['nodes'], highscores['nodes_ts'], highscores['clients'], highscores['clients_ts']))
-		if not (bot.config.ffpb.msg_target is None):
-			action_msg = 'notiert sich den neuen Highscore: {0} Knoten ({1}), {2} Clients ({3})'.format(highscores['nodes'], pretty_date(int(highscores['nodes_ts'])), highscores['clients'], pretty_date(int(highscores['clients_ts'])))
-			action_target = bot.config.ffpb.msg_target
-			if (not bot.config.ffpb.msg_target_public is None):
-				action_target = bot.config.ffpb.msg_target_public
-			bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
-
-@willie.module.commands('status')
-def ffpb_status(bot, trigger):
-	"""State of the network: count of nodes + clients"""
-
-	stats = bot.memory['ffpb_stats'] if 'ffpb_stats' in bot.memory else None
-	if stats is None:
-		bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
-		return
-
-	bot.say('Es sind {0} Knoten und ca. {1} Clients online.'.format(stats["nodes_active"], stats["clients"]))
-
 def pretty_date(time=False):
     """
     Get a datetime object or a int() Epoch timestamp and return a
@@ -848,73 +678,6 @@ def pretty_date(time=False):
         return "vor " + str(day_diff) + " Tagen"
     return "am " + compare.strftime('%d.%m.%Y um %H:%M Uhr') 
 
-@willie.module.commands('highscore')
-def ffpb_highscore(bot, trigger):
-	bot.say('Highscore: {0} Knoten ({1}), {2} Clients ({3})'.format(
-		highscores['nodes'], pretty_date(int(highscores['nodes_ts'])),
-		highscores['clients'], pretty_date(int(highscores['clients_ts']))))
-
-@willie.module.commands('rollout-status')
-def ffpb_rolloutstatus(bot, trigger):
-	"""Display statistic on how many nodes have installed the given firmware version."""
-
-	# initialize results dictionary
-	result = { }
-	skipped = 0
-
-	# inform users about changed command parameters
-	if not trigger.group(2) is None:
-		bot.reply('Dieses Kommando nimmt keinen Parameter mehr an.')
-		return
-
-	# check each node in ALFRED data
-	for nodeid in alfred_data:
-		item = alfred_data[nodeid]
-		if (not 'software' in item) or (not 'firmware' in item['software']) or (not 'autoupdater' in item['software']):
-			skipped+=1
-			continue
-
-		release = item['software']['firmware']['release']
-		branch = item['software']['autoupdater']['branch']
-		enabled = item['software']['autoupdater']['enabled']
-		if not release in result or result[release] is None:
-			result[release] = { 'stable': None, 'testing': None }
-		if not branch in result[release] or result[release][branch] is None:
-			result[release][branch] = { 'auto': 0, 'manual': 0, 'total': 0 }
-
-		result[release][branch]['total'] += 1
-		mode = 'auto' if enabled else 'manual'
-		result[release][branch][mode] += 1
-
-	# respond to user
-	releases = sorted([x for x in result])
-	for release in releases:
-		output = 'Rollout von \'{0}\':'.format(release)
-		branches = sorted([x for x in result[release]])
-		first = True
-		for branch in branches:
-			item = result[release][branch]
-			if item is None: continue
-
-			if not first:
-				output += ','
-			first = False
-
-			total = item['total']
-			auto_count = item['auto']
-			manual_count = item['manual']
-
-			output += ' {2} {0}'.format(branch, total, auto_count, manual_count)
-			if manual_count > 0:
-				output += ' (+{3} manuell)'.format(branch, total, auto_count, manual_count)
-
-		bot.say(output)
-
-	# output count of nodes for which the autoupdater's branch and/or 
-	# firmware version could not be retrieved
-	if skipped > 0:
-		bot.say('plus {0} Knoten deren Status gerade nicht abfragbar war'.format(skipped))
-
 @willie.module.commands('ping')
 def ffpb_ping(bot, trigger=None, target_name=None, reply_directly=True):
 	"""Ping the given node"""
@@ -944,14 +707,6 @@ def ffpb_ping(bot, trigger=None, target_name=None, reply_directly=True):
 		if reply_directly: bot.say('Uh oh, irgendwas ist kaputt. Chef, ping result = ' + str(result) + ' - darf ich das essen?')
 		return None
 
-@willie.module.commands('providers')
-def ffpb_providers(bot, trigger):
-	"""Fetch the top 5 providers from BATCAVE."""
-
-	providers = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/providers?format=json'))
-	providers.sort(key=lambda x: x['count'], reverse=True)
-	bot.say('Unsere Top 5 Provider: ' + ', '.join(['{0} ({1:.0f}%)'.format(x['name'], x['percentage']) for x in providers[:5]]))
-
 @willie.module.commands('mesh')
 def ffpb_nodemesh(bot, trigger):
 	"""Display mesh partners of the given node."""

+ 152 - 0
modules/ffpb_netstatus.py

@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function
+import willie
+
+import json
+import shelve
+import time
+import urllib2
+
+from ffpb import ffpb_fetch_stats, get_alfred_data, pretty_date
+
+highscores = None
+
+def setup(bot):
+	global highscores
+
+	# load highscores from disk
+	highscores = shelve.open('highscoredata', writeback=True)
+	if not 'nodes' in highscores:
+		highscores['nodes'] = 0
+		highscores['nodes_ts'] = time.time()
+	if not 'clients' in highscores:
+		highscores['clients'] = 0
+		highscores['clients_ts'] = time.time()
+
+def shutdown(bot):
+	global highscores
+
+	# store highscores
+	if not highscores is None:
+		highscores.sync()
+		highscores.close()
+		highscores = None
+
+@willie.module.interval(15)
+def ffpb_get_stats(bot):
+	"""Fetch current statistics, if the highscore changes signal this."""
+
+	(nodes_active, nodes_total, clients_count) = ffpb_fetch_stats(bot, 'http://map.paderborn.freifunk.net/nodes.json', 'ffpb_stats')
+
+	highscore_changed = False
+	if nodes_active > highscores['nodes']:
+		highscores['nodes'] = nodes_active
+		highscores['nodes_ts'] = time.time()
+		highscore_changed = True
+
+	if clients_count > highscores['clients']:
+		highscores['clients'] = clients_count
+		highscores['clients_ts'] = time.time()
+		highscore_changed = True
+
+	if highscore_changed:
+		print('HIGHSCORE changed: {0} nodes ({1}), {2} clients ({3})'.format(highscores['nodes'], highscores['nodes_ts'], highscores['clients'], highscores['clients_ts']))
+		if not (bot.config.ffpb.msg_target is None):
+			action_msg = 'notiert sich den neuen Highscore: {0} Knoten ({1}), {2} Clients ({3})'.format(highscores['nodes'], pretty_date(int(highscores['nodes_ts'])), highscores['clients'], pretty_date(int(highscores['clients_ts'])))
+			action_target = bot.config.ffpb.msg_target
+			if (not bot.config.ffpb.msg_target_public is None):
+				action_target = bot.config.ffpb.msg_target_public
+			bot.msg(action_target, '\x01ACTION %s\x01' % action_msg)
+
+@willie.module.commands('status')
+def ffpb_status(bot, trigger):
+	"""State of the network: count of nodes + clients"""
+
+	stats = bot.memory['ffpb_stats'] if 'ffpb_stats' in bot.memory else None
+	if stats is None:
+		bot.say('Uff, kein Plan wo der Zettel ist. Fragst du später nochmal?')
+		return
+
+	bot.say('Es sind {0} Knoten und ca. {1} Clients online.'.format(stats["nodes_active"], stats["clients"]))
+
+@willie.module.commands('highscore')
+def ffpb_highscore(bot, trigger):
+	bot.say('Highscore: {0} Knoten ({1}), {2} Clients ({3})'.format(
+		highscores['nodes'], pretty_date(int(highscores['nodes_ts'])),
+		highscores['clients'], pretty_date(int(highscores['clients_ts']))))
+
+@willie.module.commands('rollout-status')
+def ffpb_rolloutstatus(bot, trigger):
+	"""Display statistic on how many nodes have installed the given firmware version."""
+
+	# initialize results dictionary
+	result = { }
+	skipped = 0
+
+	# inform users about changed command parameters
+	if not trigger.group(2) is None:
+		bot.reply('Dieses Kommando nimmt keinen Parameter mehr an.')
+		return
+
+	# get ALFRED data (and ensure it is current)
+	alfred_data = get_alfred_data(bot, True)
+	if alfred_data is None:
+		bot.say('Ich habe irgendein Memo verpasst, sorry - bitte später nochmal fragen.')
+		return
+
+	# check each node in ALFRED data
+	for nodeid in alfred_data:
+		item = alfred_data[nodeid]
+		if (not 'software' in item) or (not 'firmware' in item['software']) or (not 'autoupdater' in item['software']):
+			skipped+=1
+			continue
+
+		release = item['software']['firmware']['release']
+		branch = item['software']['autoupdater']['branch']
+		enabled = item['software']['autoupdater']['enabled']
+		if not release in result or result[release] is None:
+			result[release] = { 'stable': None, 'testing': None }
+		if not branch in result[release] or result[release][branch] is None:
+			result[release][branch] = { 'auto': 0, 'manual': 0, 'total': 0 }
+
+		result[release][branch]['total'] += 1
+		mode = 'auto' if enabled else 'manual'
+		result[release][branch][mode] += 1
+
+	# respond to user
+	releases = sorted([x for x in result])
+	for release in releases:
+		output = 'Rollout von \'{0}\':'.format(release)
+		branches = sorted([x for x in result[release]])
+		first = True
+		for branch in branches:
+			item = result[release][branch]
+			if item is None: continue
+
+			if not first:
+				output += ','
+			first = False
+
+			total = item['total']
+			auto_count = item['auto']
+			manual_count = item['manual']
+
+			output += ' {2} {0}'.format(branch, total, auto_count, manual_count)
+			if manual_count > 0:
+				output += ' (+{3} manuell)'.format(branch, total, auto_count, manual_count)
+
+		bot.say(output)
+
+	# output count of nodes for which the autoupdater's branch and/or 
+	# firmware version could not be retrieved
+	if skipped > 0:
+		bot.say('plus {0} Knoten deren Status gerade nicht abfragbar war'.format(skipped))
+
+@willie.module.commands('providers')
+def ffpb_providers(bot, trigger):
+	"""Fetch the top 5 providers from BATCAVE."""
+
+	providers = json.load(urllib2.urlopen('http://[fdca:ffee:ff12:a255::253]:8888/providers?format=json'))
+	providers.sort(key=lambda x: x['count'], reverse=True)
+	bot.say('Unsere Top 5 Provider: ' + ', '.join(['{0} ({1:.0f}%)'.format(x['name'], x['percentage']) for x in providers[:5]]))
+

+ 143 - 0
modules/ffpb_nodeinfo.py

@@ -0,0 +1,143 @@
+# -*- coding: utf-8 -*-
+from __future__ import print_function
+import willie
+
+from ffpb import ffpb_findnode_from_botparam, ffpb_get_batcave_nodefield, mac2ipv6, playitsafe
+
+def setup(bot):
+	pass
+
+def shutdown(bot):
+	pass
+
+@willie.module.commands('alfred-data')
+def ffpb_peerdata(bot, trigger):
+	"""Show ALFRED data of the given node."""
+
+	# identify node or bail out
+	target_name = trigger.group(2)
+	node = ffpb_findnode_from_botparam(bot, target_name)
+	if node is None: return
+
+	# query must be a PM or as OP in the channel
+	if not playitsafe(bot, trigger, need_op=True, node=node):
+		# the check function already gives a bot reply, just exit here
+		return
+
+	# reply each key in the node's data
+	for key in node:
+		if key in [ 'hostname' ]: continue
+		bot.say("{0}.{1} = {2}".format(node['hostname'], key, str(node[key])))
+
+@willie.module.commands('info')
+def ffpb_peerinfo(bot, trigger):
+	"""Show information of the given node."""
+
+	# identify node or bail out
+	target_name = trigger.group(2)
+	node = ffpb_findnode_from_botparam(bot, target_name)
+	if node is None: return
+
+	# read node information
+	info_mac = node['network']['mac'] if 'network' in node and 'mac' in node['network'] else '??:??:??:??:??:??'
+	info_id = node['node_id'] if 'node_id' in node else info_mac.replace(':','')
+	info_name = node['hostname'] if 'hostname' in node else '?-' + info_id
+
+	info_hw = ""
+	if "hardware" in node:
+		if "model" in node["hardware"]:
+			model = node["hardware"]["model"]
+			info_hw = " model='" + model + "'"
+
+	info_fw = ""
+	info_update = ""
+	if "software" in node:
+		if "firmware" in node["software"]:
+			fwinfo = str(node["software"]["firmware"]["release"]) if "release" in node["software"]["firmware"] else "unknown"
+			info_fw = " firmware=" + fwinfo
+
+		if "autoupdater" in node["software"]:
+			autoupdater = node["software"]["autoupdater"]["branch"] if node["software"]["autoupdater"]["enabled"] else "off"
+			info_update = " (autoupdater="+autoupdater+")"
+
+	info_uptime = ""
+	u = -1
+	if "statistics" in node and "uptime" in node["statistics"]:
+		u = int(float(node["statistics"]["uptime"]))
+	elif 'uptime' in node:
+		u = int(float(node['uptime']))
+
+	if u > 0:
+		d, r1 = divmod(u, 86400)
+		h, r2 = divmod(r1, 3600)
+		m, s = divmod(r2, 60)
+		if d > 0:
+			info_uptime = ' up {0}d {1}h'.format(d,h)
+		elif h > 0:
+			info_uptime = ' up {0}h {1}m'.format(h,m)
+		else:
+			info_uptime = ' up {0}m'.format(m)
+
+	info_clients = ""
+	clientcount = ffpb_get_batcave_nodefield(info_id, 'clientcount')
+	if not clientcount is None:
+		clientcount = int(clientcount)
+		info_clients = ' clients={0}'.format(clientcount)
+
+	bot.say('[{1}]{2}{3}{4}{5}{6}'.format(info_mac, info_name, info_hw, info_fw, info_update, info_uptime, info_clients))
+
+@willie.module.commands('uptime')
+def ffpb_peeruptime(bot, trigger):
+	"""Display the uptime of the given node."""
+
+	# identify node or bail out
+	target_name = trigger.group(2)
+	node = ffpb_findnode_from_botparam(bot, target_name)
+	if node is None: return
+
+	# get name and raw uptime from node
+	info_name = node["hostname"]
+	info_uptime = ''
+	u_raw = None
+	if 'statistics' in node and 'uptime' in node['statistics']:
+		u_raw = node['statistics']['uptime']
+	elif 'uptime' in node:
+		u_raw = node['uptime']
+
+	# pretty print uptime
+	if not u_raw is None:
+		u = int(float(u_raw))
+		d, r1 = divmod(u, 86400)
+		h, r2 = divmod(r1, 3600)
+		m, s = divmod(r2, 60)
+		if d > 0:
+			info_uptime += '{0}d '.format(d)
+		if h > 0:
+			info_uptime += '{0}h '.format(h)
+		info_uptime += '{0}m'.format(m)
+		info_uptime += ' # raw: \'{0}\''.format(u_raw)
+	else:
+		info_uptime += '?'
+
+	# reply to user
+	bot.say('uptime(\'{0}\') = {1}'.format(info_name, info_uptime))
+
+@willie.module.commands('link')
+def ffpb_peerlink(bot, trigger):
+	"""Display MAC and link to statuspage for the given node."""
+
+	# identify node or bail out
+	target_name = trigger.group(2)
+	node = ffpb_findnode_from_botparam(bot, target_name)
+	if node is None: return
+
+	# get node's MAC
+	info_mac = node["network"]["mac"]
+	info_name = node["hostname"]
+
+	# get node's v6 address in the mesh (derived from MAC address)
+	info_v6 = mac2ipv6(info_mac, 'fdca:ffee:ff12:132:')
+
+	# reply to user
+	bot.say('[{1}] mac {0} -> http://[{2}]/'.format(info_mac, info_name, info_v6))
+