├── README.md └── huawei-router.py /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Huawei Router (Python) 3 | 4 | Fetching status information from the Huawei Routers (including the UMTS WiFi 5 | Hotspot E5332 and the LTE Stick E3372) can be easy. 6 | 7 | This is a tool is written in plain Python3+ w/o external dependencies. 8 | 9 | ### Example call 10 | 11 | Here is the 'help' for the CLI of the tool: 12 | 13 | 14 | [philipp@lion Huawei-Router_Python]$ ./huawei-router.py --help 15 | usage: huawei-router.py [-h] [--tree] [--flavour {default,e3372,e5332}] [host] 16 | 17 | Fetch all kinds of status information from the Huawei E5332 UMTS Hotspot. 18 | 19 | positional arguments: 20 | host The hostname / IP address your E5332 is reachable at 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | --tree Read tree-style APIs 25 | --flavour {default,e3372,e5332} 26 | Device flavour to use 27 | 28 | And here's an example call showing the output 29 | when fetching the device's status information: 30 | 31 | [philipp@lion Huawei-Router_Python]$ ./huawei-router.py --flavour e5332 192.168.1.1 32 | device_information_DeviceName: E5332 33 | device_information_SerialNumber: L5D4C13921101070 34 | device_information_Imei: 8672333678411112 35 | device_information_Imsi: 262996301120287 36 | device_information_Iccid: 89084893974902100001 37 | device_information_HardwareVersion: CH1E5331M 38 | device_information_SoftwareVersion: 21.344.19.00.1080 39 | device_information_WebUIVersion: 11.001.07.00.03 40 | device_information_MacAddress1: 00:66:4B:0C:23:DD 41 | monitoring_status_SignalStrength: 92 42 | monitoring_status_SignalIcon: 5 43 | monitoring_status_CurrentNetworkType: 9 44 | monitoring_status_CurrentServiceDomain: 3 45 | monitoring_status_BatteryStatus: 0 46 | [ . ] 47 | [ . ] 48 | [ . ] 49 | [ many more lines ] 50 | [ . ] 51 | [ . ] 52 | [ . ] 53 | pin_simlock_pSimLockRemainTimes: None 54 | wlan_host_list_1_ID: 1 55 | wlan_host_list_1_AssociatedTime: 1354 56 | wlan_host_list_1_MacAddress: e0:f8:47:d6:c2:41 57 | wlan_host_list_1_AssociatedSsid: HOTspot 58 | wlan_host_list_1_IpAddress: 192.168.1.101 59 | wlan_host_list_1_HostName: lion 60 | 61 | ### Author 62 | 63 | * Philipp Klaus 64 | 65 | -------------------------------------------------------------------------------- /huawei-router.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import xml.dom.minidom, urllib.request, urllib.parse, http.cookiejar, pprint, logging, json 4 | from datetime import datetime as dt 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | class HuaweiE5332(object): 9 | 10 | HOME_URL = 'http://{host}' 11 | BASE_URL = 'http://{host}/api/' 12 | 13 | XML_APIS = [ 14 | {'name': 'device_information', 'flavour': 'default', 'kind': 'flat', 'url': '/api/device/information'}, 15 | {'name': 'device_basic_information', 'flavour': 'e3372', 'kind': 'flat', 'url': '/api/device/basic_information'}, 16 | {'name': 'device_signal', 'flavour': 'e3372', 'kind': 'flat', 'url': '/api/device/signal'}, 17 | {'name': 'global_module_switch', 'flavour': 'default', 'kind': 'flat', 'url': '/api/global/module-switch'}, 18 | {'name': 'monitoring_status', 'flavour': 'default', 'kind': 'flat', 'url': '/api/monitoring/status'}, 19 | {'name': 'monitoring_converged_status', 'flavour': 'default', 'kind': 'flat', 'url': '/api/monitoring/converged-status'}, 20 | {'name': 'monitoring_traffic_statistics', 'flavour': 'default', 'kind': 'flat', 'url': '/api/monitoring/traffic-statistics'}, 21 | {'name': 'monitoring_check_notifications','flavour': 'default', 'kind': 'flat', 'url': '/api/monitoring/check-notifications'}, 22 | {'name': 'monitoring_start_date', 'flavour': 'e3372', 'kind': 'flat', 'url': '/api/monitoring/start_date'}, 23 | {'name': 'monitoring_month_statistics', 'flavour': 'e3372', 'kind': 'flat', 'url': '/api/monitoring/month_statistics'}, 24 | {'name': 'dialup_connection', 'flavour': 'default', 'kind': 'flat', 'url': '/api/dialup/connection'}, 25 | {'name': 'dialup_mobile_dataswitch', 'flavour': 'e3372', 'kind': 'flat', 'url': '/api/dialup/mobile-dataswitch'}, 26 | {'name': 'dialup_profiles', 'flavour': 'default', 'kind': 'tree', 'url': '/api/dialup/profiles'}, 27 | {'name': 'net_register', 'flavour': 'default', 'kind': 'flat', 'url': '/api/net/register'}, 28 | {'name': 'net_current_plmn', 'flavour': 'e3372', 'kind': 'flat', 'url': '/api/net/current-plmn'}, 29 | {'name': 'net_network', 'flavour': 'default', 'kind': 'flat', 'url': '/api/net/network'}, 30 | {'name': 'sms_config', 'flavour': 'default', 'kind': 'flat', 'url': '/api/sms/config'}, 31 | {'name': 'wlan_basic_settings', 'flavour': 'e5332', 'kind': 'flat', 'url': '/api/wlan/basic-settings'}, 32 | {'name': 'wlan_security_settings', 'flavour': 'e5332', 'kind': 'flat', 'url': '/api/wlan/security-settings'}, 33 | {'name': 'wlan_host_list', 'flavour': 'e5332', 'kind': 'tree', 'url': '/api/wlan/host-list'}, 34 | {'name': 'wlan_wps', 'flavour': 'e5332', 'kind': 'flat', 'url': '/api/wlan/wps'}, 35 | {'name': 'wlan_mac_filter', 'flavour': 'e5332', 'kind': 'flat', 'url': '/api/wlan/mac-filter'}, 36 | {'name': 'dhcp_settings', 'flavour': 'default', 'kind': 'flat', 'url': '/api/dhcp/settings'}, 37 | {'name': 'security_dmz', 'flavour': 'default', 'kind': 'flat', 'url': '/api/security/dmz'}, 38 | {'name': 'security_firewall_switch', 'flavour': 'default', 'kind': 'flat', 'url': '/api/security/firewall-switch'}, 39 | {'name': 'security_lan_ip_filter', 'flavour': 'default', 'kind': 'tree', 'url': '/api/security/lan-ip-filter'}, 40 | {'name': 'security_upnp', 'flavour': 'default', 'kind': 'flat', 'url': '/api/security/upnp'}, 41 | {'name': 'security_sip', 'flavour': 'default', 'kind': 'flat', 'url': '/api/security/sip'}, 42 | {'name': 'security_nat', 'flavour': 'default', 'kind': 'flat', 'url': '/api/security/nat'}, 43 | {'name': 'pin_status', 'flavour': 'default', 'kind': 'flat', 'url': '/api/pin/status'}, 44 | {'name': 'pin_simlock', 'flavour': 'default', 'kind': 'flat', 'url': '/api/pin/simlock'}, 45 | {'name': 'user_state_login', 'flavour': 'default', 'kind': 'flat', 'url': '/api/user/state-login'}, 46 | {'name': 'config_global', 'flavour': 'default', 'kind': 'config','url': '/config/global/config.xml'}, 47 | {'name': 'config_sms', 'flavour': 'default', 'kind': 'config','url': '/config/sms/config.xml'}, 48 | {'name': 'config_dialup_connectmode', 'flavour': 'default', 'kind': 'config','url': '/config/dialup/connectmode.xml'}, 49 | {'name': 'config_wifi_countryChannel', 'flavour': 'default', 'kind': 'config','url': '/config/wifi/countryChannel.xml'}, 50 | ] 51 | 52 | def __init__(self, flavour='default', host=None): 53 | if host is None: 54 | host = '192.168.1.1' 55 | self.host = host 56 | self.flavour = flavour 57 | self.home_url = self.HOME_URL.format(host=host) 58 | cj = http.cookiejar.CookieJar() 59 | self.opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) 60 | self.opener.open(self.home_url) 61 | self.base_url = self.BASE_URL.format(host=host) 62 | 63 | def relevant_apis(self): 64 | return [api for api in self.XML_APIS if api['flavour'] in ('default', self.flavour)] 65 | 66 | def _api_by_name(self, name): 67 | return [api for api in self.relevant_apis() if api['name'] == name][0] 68 | 69 | def _get(self, url): 70 | with self.opener.open(url) as response: 71 | rtext = response.read() 72 | return rtext 73 | 74 | def wlan_host_list(self): 75 | host_list = [] 76 | api = self._api_by_name('wlan_host_list') 77 | url = urllib.parse.urljoin(self.base_url, api['url']) 78 | dom = xml.dom.minidom.parseString(self._get(url)) 79 | get_first_response(dom, api) 80 | hosts = dom.getElementsByTagName("Hosts")[0] 81 | for host in hosts.childNodes: 82 | if type(host) is not xml.dom.minidom.Element: continue 83 | host_dict = {} 84 | for prop in host.childNodes: 85 | if type(prop) is not xml.dom.minidom.Element: continue 86 | prop, val = prop.tagName, prop.firstChild.data if prop.firstChild is not None else None 87 | host_dict[prop] = val 88 | host_list.append(host_dict) 89 | return host_list 90 | 91 | def tree_properties(self): 92 | properties = [] 93 | tree_apis = [api for api in self.relevant_apis() if api['kind'] == 'tree'] 94 | for api in tree_apis: 95 | url = urllib.parse.urljoin(self.base_url, api['url']) 96 | dom = xml.dom.minidom.parseString(self._get(url)) 97 | try: 98 | response = get_first_response(dom, api) 99 | except InvalidResponseException: 100 | continue 101 | properties.append((api['name'], HuaweiE5332.recursive_checknodes(response))) 102 | return properties 103 | 104 | def recursive_checknodes(node): 105 | """ 106 | A class method to go through the XML tree structures 107 | as provided by the E5332. Returns a dict(). 108 | Similar to: http://stackoverflow.com/a/10231610/183995 109 | """ 110 | ret_dict = {} 111 | childlen = len(node.childNodes) 112 | for cnode in node.childNodes: 113 | if len(cnode.childNodes) == 1: 114 | ret_dict[cnode.tagName] = cnode.firstChild.data if cnode.firstChild is not None else None 115 | if ret_dict[cnode.tagName].strip() == '': ret_dict[cnode.tagName] = None 116 | continue 117 | if cnode.firstChild is None: continue 118 | val = HuaweiE5332.recursive_checknodes(cnode) 119 | if type(ret_dict) is list or cnode.tagName in ret_dict or cnode.tagName + 's' == node.tagName: 120 | try: 121 | ret_dict.append(val) 122 | except AttributeError: 123 | try: 124 | ret_dict = [ret_dict[cnode.tagName], val] 125 | except KeyError: 126 | ret_dict = [val] 127 | else: 128 | ret_dict[cnode.tagName] = val 129 | return ret_dict 130 | 131 | def flat_properties(self): 132 | properties = [] 133 | flat_apis = [api for api in self.relevant_apis() if api['kind'] == 'flat'] 134 | for api in flat_apis: 135 | url = urllib.parse.urljoin(self.base_url, api['url']) 136 | dom = xml.dom.minidom.parseString(self._get(url)) 137 | try: 138 | response = get_first_response(dom, api) 139 | except InvalidResponseException: 140 | continue 141 | for cn in response.childNodes: 142 | if type(cn) is not xml.dom.minidom.Element: continue 143 | prop, val = cn.tagName, cn.firstChild.data if cn.firstChild is not None else None 144 | properties.append((api['name'] + '_' + prop, val)) 145 | if 'wlan_host_list' in [api['name'] for api in self.relevant_apis()]: 146 | for host in self.wlan_host_list(): 147 | hid = host['ID'] 148 | for key in host: 149 | properties.append(('wlan_host_list' + '_' + hid + '_' + key, host[key])) 150 | return properties 151 | 152 | def get_first_response(dom, context): 153 | try: 154 | return dom.getElementsByTagName("response")[0] 155 | except IndexError: 156 | logger.warning('Could not parse API {name}\n'.format(**context)) 157 | raise InvalidResponseException(context['name']) 158 | 159 | class InvalidResponseException(NameError): pass 160 | 161 | def main(): 162 | import argparse 163 | parser = argparse.ArgumentParser(description='Fetch all kinds of status information from the Huawei E5332 UMTS Hotspot.') 164 | parser.add_argument('host', nargs='?', default='192.168.1.1', help='The hostname / IP address your E5332 is reachable at') 165 | parser.add_argument('--tree', action='store_true', help='Read tree-style APIs') 166 | parser.add_argument('--format', choices=('json','text'), help='Output format', default='text') 167 | def flavours(): return set([api['flavour'] for api in HuaweiE5332.XML_APIS]) 168 | parser.add_argument('--flavour', choices=flavours(), help='Device flavour to use') 169 | 170 | logging.basicConfig(level=logging.INFO) 171 | 172 | args = parser.parse_args() 173 | e5332 = HuaweiE5332(flavour=args.flavour, host=args.host) 174 | if args.tree: 175 | for prop, val in e5332.tree_properties(): 176 | print("\n--------------\n{}:\n".format(prop)) 177 | pprint.pprint(val) 178 | else: 179 | fp = e5332.flat_properties() 180 | if args.format == 'text': 181 | for prop, val in fp: 182 | print("{}: {}".format(prop, val)) 183 | elif args.format == 'json': 184 | fp = dict(fp) 185 | print(json.dumps({'data': fp, 'timestamp': dt.now().isoformat()})) 186 | 187 | if __name__ == "__main__": 188 | main() 189 | --------------------------------------------------------------------------------