From a107d29e288d1c33cc8b5c2e288d05ba5fffea56 Mon Sep 17 00:00:00 2001 From: Darko Lukic Date: Wed, 14 Mar 2018 22:17:06 +0100 Subject: [PATCH] Add histogram and data export REST API --- frontend/README.md | 9 +- frontend/app.py | 4 + frontend/config.py | 2 +- frontend/data.py | 58 ++++++ frontend/histogram.py | 94 +++++++++ frontend/web_ui.py | 467 ------------------------------------------ frontend/wifi.py | 4 +- 7 files changed, 167 insertions(+), 471 deletions(-) create mode 100644 frontend/data.py create mode 100644 frontend/histogram.py delete mode 100644 frontend/web_ui.py diff --git a/frontend/README.md b/frontend/README.md index b3b5c8a..0e41a17 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -5,4 +5,11 @@ Make sure that CosmicPi UI service does not run (`sudo systemctl stop CosmicPi-U navigate to `/frontend` and run: ``` FLASK_DEBUG=1 FLASK_APP=${PWD}/app.py python -m flask run --host=0.0.0.0 -``` \ No newline at end of file +``` + +## API +- GET `/histogram.png?start_time=0&end_time=1521059693&bin_size_seconds=1` +- GET `/params` +- GET `/wifi` +- PUT `/wifi?ssid=CosmicPi&pass=12345678` +- GET `/data?format=csv` \ No newline at end of file diff --git a/frontend/app.py b/frontend/app.py index 6d7c927..423bd2a 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -2,12 +2,16 @@ from flask import Flask, request from flask_restful import Api from .params import Params from .wifi import Wifi +from .histogram import Histogram +from .data import Data app = Flask(__name__) api = Api(app) api.add_resource(Params, '/params') api.add_resource(Wifi, '/wifi') +api.add_resource(Histogram, '/histogram.png') +api.add_resource(Data, '/data') if __name__ == '__main__': app.run(debug=True) diff --git a/frontend/config.py b/frontend/config.py index 74c13ae..f81e855 100644 --- a/frontend/config.py +++ b/frontend/config.py @@ -2,4 +2,4 @@ import configparser Config = configparser.ConfigParser() -Config.read('../config/CosmicPi.config') \ No newline at end of file +Config.read('../config/CosmicPi.config') diff --git a/frontend/data.py b/frontend/data.py new file mode 100644 index 0000000..fa61c2d --- /dev/null +++ b/frontend/data.py @@ -0,0 +1,58 @@ +from flask import request, make_response +from flask_restful import Resource +from .config import Config +import sqlite3 +import io +import csv + + +SQLITE_LOCATION = Config.get("Storage", "sqlite_location") + + +class Data(Resource): + def get(self): + format = request.args['format'] + + conn = sqlite3.connect(SQLITE_LOCATION, timeout=60.0) + cursor = conn.cursor() + + # Get column names + cursor.execute("PRAGMA table_info(Events);") + col_data = cursor.fetchall() + col_names = [] + for i in range(0, len(col_data)): + col_names.append(col_data[i][1]) + + # Get data from database + cursor.execute("SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC;") + data = cursor.fetchall() + conn.close() + + if format == 'json': + return Data._get_json(col_names, data) + elif format == 'csv': + return Data._get_csv(col_names, data) + + @staticmethod + def _get_json(col_names, data): + items = [] + for row in data: + item = {} + for i in range(len(col_names)): + item[col_names[i]] = row[i] + items.append(item) + return items + + @staticmethod + def _get_csv(col_names, data): + """ + Write CSV export to memory + """ + output = io.BytesIO() + writer = csv.writer(output) + writer.writerow(col_names) + writer.writerows(data) + + response = make_response(output.getvalue()) + response.headers['Content-Type'] = 'text/csv' + return response diff --git a/frontend/histogram.py b/frontend/histogram.py new file mode 100644 index 0000000..2b0ee80 --- /dev/null +++ b/frontend/histogram.py @@ -0,0 +1,94 @@ +import matplotlib +matplotlib.use('Agg') +from flask import request, make_response +from flask_restful import Resource +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +import io +import sqlite3 +from .config import Config + + +SQLITE_LOCATION = Config.get("Storage", "sqlite_location") + + +class Histogram(Resource): + def get(self): + start_time = int(request.args['start_time']) + end_time = int(request.args['end_time']) + bin_size_seconds = int(request.args['bin_size_seconds']) + + # render the plot + img = build_histogram(start_time, end_time, bin_size_seconds) + + # return the plot + response = make_response(img) + response.headers['Content-Type'] = 'image/png' + + return response + + +def build_histogram(start_time, end_time, bin_size_seconds): + plot_title = '' + + # get some data + data = [] + conn = sqlite3.connect(SQLITE_LOCATION, timeout=60.0) + cursor = conn.cursor() + # only get the last n seconds if the start time was negative + if start_time < 0: + plot_title += "Histogram of events over the last {0:.1f} minutes\nbin size: {1:d} [s]".format(-start_time / 60., + bin_size_seconds) + cursor.execute("SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC;") + start_time = cursor.fetchone()[0] + start_time + end_time = 9000000000 + else: + plot_title += "Histogram of events over a set time\nbin size: " + str(bin_size_seconds) + " [s]" + cursor.execute("SELECT * FROM Events WHERE UTCUnixTime BETWEEN ? AND ? ORDER BY UTCUnixTime DESC, SubSeconds DESC;", + (start_time, end_time)) + data = cursor.fetchall() + conn.close() + + # massage data + if len(data) == 0: + plt.hist([]) + plt.title("No data to display") + else: + event_time_list = [data[i][0] + data[i][1] for i in range(len(data))] + # event_time_list = [data[i][0] for i in range(len(data))] + bin_edges = range(int(event_time_list[len(event_time_list) - 1]), int(event_time_list[0]), bin_size_seconds) + x_axis_limits = (start_time, int(event_time_list[0]) + 1) + # convert our unix timestamps to Matplotlib format + event_time_list = mdates.epoch2num(event_time_list) + bin_edges = mdates.epoch2num(bin_edges) + x_axis_limits = mdates.epoch2num(x_axis_limits) + + # make the plot + plt.hist(event_time_list, bins=bin_edges) + plt.title(plot_title) + plt.xlabel("Time [UTC]") + plt.ylabel("Number of Events per {0:d} seconds [1]".format(bin_size_seconds)) + + plt.subplots_adjust(bottom=0.2) + plt.xticks(rotation=25) + plt.tick_params(which='both', width=2, direction="out", top=False, right=False) + plt.tick_params(which='major', length=5) + plt.tick_params(which='minor', length=3, color='r') + + # do the date formatting + ax = plt.gca() + locator = mdates.AutoDateLocator(minticks=7) + locator.intervald[mdates.SECONDLY] = [1, 10, 30] + formatter = mdates.AutoDateFormatter(locator) + formatter.scaled[1 / (24. * 60.)] = '%H:%M:%S' + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + ax.set_xlim(x_axis_limits) + + # return the generated plot + img = io.BytesIO() + plt.savefig(img, format='png') + img.seek(0) + plt.close() + return img.getvalue() + diff --git a/frontend/web_ui.py b/frontend/web_ui.py deleted file mode 100644 index 773940b..0000000 --- a/frontend/web_ui.py +++ /dev/null @@ -1,467 +0,0 @@ -import matplotlib -matplotlib.use('Agg') -from flask import Flask, flash, redirect, render_template, request, make_response -import matplotlib.pyplot as plt -import io -import sqlite3 -import matplotlib.dates as mdates -import csv -from flask_basicauth import BasicAuth -import configparser -import subprocess -import urllib2 -import time -import thread -import re -import socket -import struct - - - -# read settings -CONFIG_FILE = "../config/CosmicPi.config" -# read configuration -# Todo: Put the config parser into a propper class -# Todo: Implement proper error catching for configparser (e.g. non existent keys or file) -# read configuration -config = configparser.ConfigParser() -config.read(CONFIG_FILE) -SQLITE_LOCATION = config.get("Storage", "sqlite_location") -UI_USER = config.get("UI", "username") -UI_PASS = config.get("UI", "password") -DEFAULT_WIFI_NAME = config.get("Default WiFi", "name") -DEFAULT_WIFI_PASS = config.get("Default WiFi", "password") -WPA_SUPPLICANT_LOCATION = str(config.get("MISC", "wpa_supplicant_location")) - -# start flask -app = Flask(__name__) -app.config['BASIC_AUTH_USERNAME'] = UI_USER -app.config['BASIC_AUTH_PASSWORD'] = UI_PASS - -basic_auth = BasicAuth(app) - - -def initDB(): - conn = sqlite3.connect(SQLITE_LOCATION, timeout=60.0) - cursor = conn.cursor() - cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='Events'") - if cursor.fetchone() == None: - cursor.execute('''CREATE TABLE Events - (UTCUnixTime INTEGER, SubSeconds REAL, TemperatureC REAL, Humidity REAL, AccelX REAL, - AccelY REAL, AccelZ REAL, MagX REAL, MagY REAL, MagZ REAL, Pressure REAL, Longitude REAL, - Latitude REAL, DetectorName TEXT, DetectorVersion TEXT);''') - conn.commit() - -@app.route("/base/") -def base(): - return render_template( - 'cosmic_base.html', **locals()) - - -@app.route("/hw_serial.txt") -def getserial(): - # Extract serial from cpuinfo file - cpuserial = "0000000000000000" - try: - f = open('/proc/cpuinfo','r') - for line in f: - if line[0:6]=='Serial': - cpuserial = line[10:26] - f.close() - except: - cpuserial = "ERROR000000000" - - return cpuserial - - - -icon_dict = { - 'UTCUnixTime': "fa fa-clock-o fa-5x", - 'TemperatureC': "fa fa-thermometer-half fa-5x", - 'Humidity': "fa fa-tint fa-5x", - 'AccelX': "fa fa-tachometer fa-5x", - 'AccelY': "fa fa-tachometer fa-5x", - 'AccelZ': "fa fa-tachometer fa-5x", - 'MagX': "fa fa-compass fa-5x", - 'MagY': "fa fa-compass fa-5x", - 'MagZ': "fa fa-compass fa-5x", - 'Pressure': "fa fa-thermometer-half fa-5x", - 'Longitude': "fa fa-map-marker fa-5x", - 'Latitude': "fa fa-map-marker fa-5x", - 'DetectorName': "fa fa-info-circle fa-5x", - 'DetectorVersion': "fa fa-info-circle fa-5x", -} - -@app.route('/', methods=['GET', 'POST']) -@app.route('/dashboard/', methods=['GET', 'POST']) -def dashboard_page(): - values_to_display =[{'name':'Hardware Serial', 'value':getserial(), 'icon':'fa fa-microchip fa-5x'}] - - # get the latest datapoint - conn = sqlite3.connect(SQLITE_LOCATION, timeout=60.0) - cursor = conn.cursor() - cursor.execute("SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC;") - latest_datapoint = cursor.fetchone() - # get collumn names - cursor.execute("PRAGMA table_info(Events);") - col_names = cursor.fetchall() - conn.close() - - # extract data - for i in range(0,len(col_names)): - # skip the subseconds - if i == 1: - continue; - # field name - f_name = col_names[i][1] - # fill in values - if f_name in icon_dict.keys(): - values_to_display.append({'name':f_name, 'value':latest_datapoint[i], 'icon':icon_dict[f_name]}) - - return render_template('dashboard.html', values_to_display=values_to_display) - - -@app.route('/plotting/', methods=['GET', 'POST']) -def plotting_page(): - location_vars = {'Latitude': 0, 'Longitude': 0} - - # get the latest datapoint - conn = sqlite3.connect(SQLITE_LOCATION, timeout=60.0) - cursor = conn.cursor() - cursor.execute("SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC;") - latest_datapoint = cursor.fetchone() - # get collumn names - cursor.execute("PRAGMA table_info(Events);") - col_names = cursor.fetchall() - conn.close() - - # extract data - for i in range(0,len(col_names)): - # skip the subseconds - if i == 1: - continue; - # field name - f_name = col_names[i][1] - # fill in location - if f_name in location_vars.keys(): - location_vars[f_name] = latest_datapoint[i] - - - - return render_template('plotting.html', location_vars=location_vars) - -@app.route('/settings/', methods=['GET', 'POST']) -@basic_auth.required -def settings_page(): - current_WiFi, avail_WiFi = get_current_and_available_networks() - return render_template('settings.html', available_wifis=avail_WiFi, current_wifi=current_WiFi) - -def get_current_and_available_networks(): - wifiNetworkList = '' - # Presently connected network - connectedNetworkNameResponse = '' - try: - connectedNetworkNameResponse = subprocess.check_output(['sudo','iwgetid']) - except subprocess.CalledProcessError as e: - print('ERROR get connected network: ') - print(e) - except WindowsError as e: - print("Well, windows just can't do this...") - connectedNetworkNameResponse = '"No networks found, because the WebUI is beeing executed on windows."' - connectedNetworkNameStr = re.findall('\"(.*?)\"', connectedNetworkNameResponse) #" Find the string between quotes - if len(connectedNetworkNameStr) < 1: - connectedNetworkNameStr = '' - else: - connectedNetworkNameStr =connectedNetworkNameStr[0] - wifiNetworkList = [connectedNetworkNameStr] - # Available networks - availableNetworksResponse = '' - try: - availableNetworksResponse = subprocess.check_output(['sudo','iw','dev','wlan0','scan']) - except subprocess.CalledProcessError as e: - print('ERROR get list of networks: ') - print(e) - except WindowsError as e: - print("Well, windows just can't do this either...") - availableNetworksResponse = 'SSID: Network A\nSSID: Network B\n' - availableNetworksLines = availableNetworksResponse.split('\n') - for availableNetworksLine in availableNetworksLines: - if 'SSID' in availableNetworksLine: - # Typical line: - # SSID: elithion belkin - essid = availableNetworksLine.replace('SSID:','').strip() - wifiNetworkList.append(essid) - return connectedNetworkNameStr, wifiNetworkList - -@app.route('/connect_to_wifi', methods=['GET', 'POST']) -@basic_auth.required -def wifi_connector(): - # get user set parameters - wifi_name = request.args.get('selected_wifi') - wifi_pw = request.args.get('password') - - # launch the wifi login in a different thread - # we need to to it like this, since otherwise the user would never recieve an answer... - thread.start_new_thread(connect_to_wifi, (wifi_name, wifi_pw)) - - msg = 'The CosmicPi will now try to connect to the WiFi "{}". '.format(wifi_name) - msg += "If no internet connection is found or the connection was not sucessfull the CosmiPi will recreate the WiFi hotspot. Please wait at least two minutes." - return msg - - -def get_ip_address(ifname): - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - import fcntl - result = socket.inet_ntoa(fcntl.ioctl( - s.fileno(), - 0x8915, # SIOCGIFADDR - struct.pack('256s', ifname[:15]) - )[20:24]) - except IOError: - print("Error getting the IP address, either you are not doing this on raspbian or the queried device does not exist.") - result = "no IP on {}".format(ifname) - except WindowsError: - print("Getting the IP address on Windows is not implemented") - result = "no IP on {}".format(ifname) - except ImportError: - print("Getting the IP address on this OS is not implemented") - result = "no IP on {}".format(ifname) - return result - - -def fall_back_to_ap(): - # empty the wpa supplicant to it's default - wpa_supplicant_string = "country=GB\n" # Todo: Check, that this string in front is still correct! - wpa_supplicant_string += "ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\n" - wpa_supplicant_string += "update_config=1\n" - wpa_supplicant_string += "\nnetwork={\n" - net = (DEFAULT_WIFI_NAME, DEFAULT_WIFI_PASS) - wpa_supplicant_string += '\tssid="{}"\n'.format(net[0]) - # check if we need a password - if str(net[1]) == "": - wpa_supplicant_string += '\tpsk="{}"\n'.format(net[1]) - wpa_supplicant_string += "}\n" - with open(WPA_SUPPLICANT_LOCATION, 'w') as file: - file.write(wpa_supplicant_string) - # configure controler to accept the new configuration - try: - import fcntl - time.sleep(2) - subprocess.call("wpa_cli -i wlan0 reconfigure", shell=True) - time.sleep(2) - except ImportError: - print("This OS is not linux enough to handle a hotspot in this way") - - # restart the hotspot - try: - import fcntl - subprocess.call("systemctl start dnsmasq", - shell=True) # well, here we actually care if the hotspot starts, but oh well... - time.sleep(2) - subprocess.call("ifconfig wlan0 down", shell=True) - time.sleep(2) - subprocess.call("ifconfig wlan0 up", shell=True) - except ImportError: - print("This OS is not linux enough to handle a hotspot in this way") - -def connect_to_wifi(name, pw): - # build the string for the WPA supplicant - wpa_supplicant_string = "country=GB\n" # Todo: Check, that this string in front is still correct! - wpa_supplicant_string += "ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\n" - wpa_supplicant_string += "update_config=1\n" - networks = [(name, pw), (DEFAULT_WIFI_NAME, DEFAULT_WIFI_PASS)] - for net in networks: - wpa_supplicant_string += "\nnetwork={\n" - wpa_supplicant_string += '\tssid="{}"\n'.format(net[0]) - # check if we need a password - if not str(net[1]) == "": - wpa_supplicant_string += '\tpsk="{}"\n'.format(net[1]) - wpa_supplicant_string += "}\n" - - # deactivate htospot and turn the WiFi back on - try: - import fcntl - subprocess.call("systemctl stop dnsmasq", shell=True) # we very much don't care if this fails - time.sleep(2) - subprocess.call("ifconfig wlan0 down", shell=True) - time.sleep(2) - subprocess.call("ifconfig wlan0 up", shell=True) - except ImportError: - print("This OS is not linux enough to handle a hotspot in this way") - # write wpa_supplicant string to the file - with open(WPA_SUPPLICANT_LOCATION, 'w') as file: - file.write(wpa_supplicant_string) - # configure controler to accept the new configuration - try: - import fcntl - time.sleep(2) - subprocess.call("wpa_cli -i wlan0 reconfigure", shell=True) - except ImportError: - print("This OS is not linux enough to handle a hotspot in this way") - - # wait for an internet connection (max 2 min) - start_time = time.time() - have_internet = False - while (have_internet == False) and ((start_time + 120) > time.time()): - have_internet = internet_on() - - # if we have no internet, restart the hotspot, otherwise we are done for now - if have_internet: - print("Sucessfully connected to the internet (yeah)") - # wait a bit before we send the mail - time.sleep(5) - # ToDo: This would be a good point to send a mail or something similar to the user. - # Just to inform them where their cosmicPi is and what's it doing - return - else: - print("No internet connection here, falling back to hotspot!") - fall_back_to_ap() - return - -def internet_on(): - try: - urllib2.urlopen('http://heise.de', timeout=2) - return True - except urllib2.URLError as err: - return False - -@app.route('/CosmicPi_data.csv', methods=['GET']) -@basic_auth.required -def csv_export(): - conn = sqlite3.connect(SQLITE_LOCATION, timeout=60.0) - cursor = conn.cursor() - - # get collumn names - cursor.execute("PRAGMA table_info(Events);") - col_data = cursor.fetchall() - col_names = [] - for i in range(0, len(col_data)): - col_names.append(col_data[i][1]) - - # get data from db - cursor.execute("SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC;") - data = cursor.fetchall() - conn.close() - - # write CSV export to memory - output = io.BytesIO() - writer = csv.writer(output) - writer.writerow(col_names) - writer.writerows(data) - - response = make_response(output.getvalue()) - response.headers['Content-Type'] = 'text/csv' - return response - - -@app.route('/about/', methods=['GET', 'POST']) -def about_page(): - return render_template('about.html') - - -# returns the histogram as a png byte stream -def build_histogram(start_time, end_time, bin_size_seconds): - plot_title = '' - - # get some data - data = [] - conn = sqlite3.connect(SQLITE_LOCATION, timeout=60.0) - cursor = conn.cursor() - # only get the last n seconds if the start time was negative - if start_time < 0: - plot_title += "Histogram of events over the last {0:.1f} minutes\nbin size: {1:d} [s]".format(-start_time / 60., - bin_size_seconds) - cursor.execute("SELECT * FROM Events ORDER BY UTCUnixTime DESC, SubSeconds DESC;") - start_time = cursor.fetchone()[0] + start_time - end_time = 9000000000 - else: - plot_title += "Histogram of events over a set time\nbin size: " + str(bin_size_seconds) + " [s]" - cursor.execute("SELECT * FROM Events WHERE UTCUnixTime BETWEEN ? AND ? ORDER BY UTCUnixTime DESC, SubSeconds DESC;", - (start_time, end_time)) - data = cursor.fetchall() - conn.close() - - # massage data - if len(data) == 0: - plt.hist([]) - plt.title("No data to display") - else: - event_time_list = [data[i][0] + data[i][1] for i in range(len(data))] - # event_time_list = [data[i][0] for i in range(len(data))] - bin_edges = range(int(event_time_list[len(event_time_list) - 1]), int(event_time_list[0]), bin_size_seconds) - x_axis_limits = (start_time, int(event_time_list[0]) + 1) - # convert our unix timestamps to Matplotlib format - event_time_list = mdates.epoch2num(event_time_list) - bin_edges = mdates.epoch2num(bin_edges) - x_axis_limits = mdates.epoch2num(x_axis_limits) - - # make the plot - plt.hist(event_time_list, bins=bin_edges) - plt.title(plot_title) - plt.xlabel("Time [UTC]") - plt.ylabel("Number of Events per {0:d} seconds [1]".format(bin_size_seconds)) - - plt.subplots_adjust(bottom=0.2) - plt.xticks(rotation=25) - plt.tick_params(which='both', width=2, direction="out", top=False, right=False) - plt.tick_params(which='major', length=5) - plt.tick_params(which='minor', length=3, color='r') - - # do the date formatting - ax = plt.gca() - locator = mdates.AutoDateLocator(minticks=7) - locator.intervald[mdates.SECONDLY] = [1, 10, 30] - formatter = mdates.AutoDateFormatter(locator) - formatter.scaled[1 / (24. * 60.)] = '%H:%M:%S' - ax.xaxis.set_major_locator(locator) - ax.xaxis.set_major_formatter(formatter) - ax.set_xlim(x_axis_limits) - - # return the generated plot - img = io.BytesIO() - plt.savefig(img, format='png') - img.seek(0) - plt.close() - return img.getvalue() - -@app.route('/histogram.png') -def serve_histogram_request(): - # get user set parameters - start_time = request.args.get('start_time', type=int) - end_time = request.args.get('end_time', type=int) - bin_size_seconds = request.args.get('bin_size_seconds', type=int) - - # render the plot - img = build_histogram(start_time, end_time, bin_size_seconds) - - # return the plot - response = make_response(img) - response.headers['Content-Type'] = 'image/png' - return response - -def periodically_render_dashboard_histogram(): - while True: - # standard values for the dashboard histogram - start_time = -120 - end_time = 9000000000 - bin_size_seconds = 2 - # place where we need to save the image - path_to_static_image = "static/images/dashboard_histogram.png" - - # render the plot - img = build_histogram(start_time, end_time, bin_size_seconds) - - # save the image to disk - with open(path_to_static_image, 'wb') as f: - f.write(img) - - # wait four seconds until we render the next plot - time.sleep(4) - -if __name__ == '__main__': - # do necessary inits - initDB() - # Launch the periodical histogram renderer for the dashboard - thread.start_new_thread(periodically_render_dashboard_histogram, ()) - app.run() diff --git a/frontend/wifi.py b/frontend/wifi.py index 73d839a..15e1c42 100644 --- a/frontend/wifi.py +++ b/frontend/wifi.py @@ -54,7 +54,7 @@ class Wifi(Resource): def put(self): ssid = request.form['ssid'] - password = request.form['password'] + password = request.form['pass'] thread.start_new_thread(connect_to_wifi, (ssid, password)) @@ -141,7 +141,7 @@ def connect_to_wifi(name, pw): # wait for an internet connection (max 2 min) start_time = time.time() have_internet = False - while (have_internet == False) and ((start_time + 120) > time.time()): + while not have_internet and ((start_time + 120) > time.time()): have_internet = internet_on() # if we have no internet, restart the hotspot, otherwise we are done for now