Today we will be building a python script to show Huawei 4G Hostspot battery percentage in mac status bar and which will also play an alert when the battery is low.
I was in a zoom meeting the other day and the call got abruptly disrupted because my wifi dongle got turned off due to low battery. Unfortunately there is no plug point available where i get the best network coverage so i can’t leave the dongle plugged in to power outlet. You may suggest me to get a extension cord rather than building a specific tool for this , but where is the fun in it :-)
The status page
The dongle provides a simple web page to show status and to modify settings, this can be accessed by visiting http://192.168.2.1 The landing page contains basic information showing battery, wifi status and network coverage.
Luckily to show this basic info it does not require any password. When battery status changes the icon would change accordingly, so my initial though was to use python requests library and find the battery status icon to get the percentage. The page disables right click, but you can use command-option-I to open developer setting in chrome. Inspecting the battery icon shows the below src.
<li id="battery_gif" class="nav07"> <img onload="fixPNG(this)" src="../res/battery_level_4.png" 0="" no-repeat=""> </li>
I wrote this small snippet, to grab the content of the status page.
import requests resp = requests.get('http://192.168.2.1/html/home.html') print(resp.text)
Endpoint to get device status
So a get request to http://192.168.2.1/api/monitoring/status would return all the status details in xml format. I wish every device and service would provide a rest API to tinker and customize. So lets modify the above snippet to send a get request to the new url.
import requests resp = requests.get('http://192.168.2.1/api/monitoring/status') print(resp.text)
But the response was different here, it was an error.
<?xml version="1.0" encoding="UTF-8"?> <error> <code>125002</code> <message></message> </error>
I copied the curl request from dev tool, to see whether there were some other fields/headers being sent and there it was, the session id in the cookie.
Dealing with sessions
A quick googling showed that requests library supports creating and using sessions for sending http requests. Just needed to modify a line in our snippet. But the catch is session id will be initialized on the home page only and not on api endpoint. So we need to send a request to home page url and then the status endpoint.
import requests req = requests.Session() req.get('http://192.168.2.1') resp = req.get('http://192.168.2.1/api/monitoring/status') print(resp.text)
Parsing xml and alerting on low battery
We use ElementTree XML library to parse XML, the modified code to print battery percent is below
import requests import xml.etree.ElementTree as ET req = requests.Session() req.get('http://192.168.2.1') resp = req.get('http://192.168.2.1/api/monitoring/status') xml_resp = ET.fromstring(resp.text) batt_percent = int(xml_resp.find('BatteryPercent').text) print(batt_percent)
Now that we have battery percentage available, lets notify on low battery percentage. I did not want to have another audio file in the snippet and wanted to find a very simple way of notify with sound. There was one way where we can have base64 encoded beep sound in the snippet and play that to notify. But i found a interesting fact that printing ‘\a’ to unix based terminal played a kind of ding sound which was more than enough for our need.
Here is the new snippet
import requests import xml.etree.ElementTree as ET from time import sleep req = requests.Session() while true: req.get('http://192.168.2.1') resp = req.get('http://192.168.2.1/api/monitoring/status') xml_resp = ET.fromstring(resp.text) batt_percent = int(xml_resp.find('BatteryPercent').text) if batt_percent<=25: #beep 5 times print('\a'*5)
Status bar app with rumps
Now that we know it works, lets clean this up and turn it into an status bar app. We will be using rumps library. Rumps allows us to set the icon and title of the status bar app at runtime. We will be using icon to indicate whether the laptop is connected to hotspot device and title of status bar will hold the percentage.
import xml.etree.ElementTree as ET from time import sleep import requests import rumps class BatteryStatusApp(rumps.App): connected_icon = 'connected_icon.png' disconnected_icon = 'disconnected_icon.png' url = 'http://192.168.1.1' status_url = url + "/api/monitoring/status" timeouts = (3, 3) req = None threshold = 30 # initialize session def init_session(self): self.req = requests.Session() self.req.get(self.url) def get_battery_status(self, retry_count): """ function which returns battery charging status and battery percent status :param retry_count: how many times to retry getting status before rasing error """ resp = self.req.get(self.status_url, timeout=self.timeouts) xml_resp = ET.fromstring(resp.text) # if error, try re initializing session if xml_resp.tag == 'error': if retry_count >= 0: self.init_session() return self.get_battery_status(retry_count-1) else: raise SessionInitializationError return bool(int(xml_resp.find('BatteryStatus').text)), int(xml_resp.find('BatteryPercent').text) def __init__(self): super(BatteryStatusApp, self).__init__("", icon=self.disconnected_icon) self.req = requests.Session() @rumps.timer(60) def update_battery(self, sender): try: battery_charging, battery_percent = self.get_battery_status(3) print(battery_percent) print(battery_charging) if battery_percent < self.threshold and not battery_charging: rumps.notification( "Battery Status", "Battery low", str(battery_percent)) self.title = str(battery_percent)+"%" self.icon = self.connected_icon except Exception as e: self.title = "" self.icon = self.disconnected_icon class NoConnectionError(Exception): """ Unable to get battery status, may not be connected to hotspot""" class SessionInitializationError(Exception): """Unable to get battery status""" if __name__ == "__main__": BatteryStatusApp().run()
Thanks to rumps, the code is pretty simple. In constructor we initialize the session icon and title. We have just added some timeouts and error handling to our above code. Rather than calling req.get(‘http://192.168.2.1’) everytime, we call it during initialization and when there is an error. We update the battery status every 60 seconds by using inbuilt rumps decorator
@rumps.timer(60) def update_battery(self, sender): ....
And when battery status is low, we send a notification every minute untill the device is plugged in. Here’s how it looks.