├── custom_components └── padavan_tracker │ ├── __init__.py │ ├── manifest.json │ └── device_tracker.py ├── hacs.json └── README.md /custom_components/padavan_tracker/__init__.py: -------------------------------------------------------------------------------- 1 | """The Padavan Tracker component.""" 2 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Padavan Tracker", 3 | "render_readme": true, 4 | "domains": ["device_tracker"], 5 | "iot_class": ["Local Polling"] 6 | } 7 | -------------------------------------------------------------------------------- /custom_components/padavan_tracker/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "padavan_tracker", 3 | "name": "Padavan Tracker", 4 | "documentation": "https://github.com/PaulAnnekov/home-assistant-padavan-tracker", 5 | "issue_tracker": "https://github.com/PaulAnnekov/home-assistant-padavan-tracker/issues", 6 | "dependencies": [], 7 | "codeowners": ["@PaulAnnekov"], 8 | "version": "1.0.2", 9 | "iot_class": "local_polling" 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant Padavan Device Tracker 2 | 3 | [![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/hacs/integration) 4 | 5 | This device tracker component allows you to get **wireless** devices presence from 6 | [Padavan](https://bitbucket.org/padavan/rt-n56u)-based routers. 7 | 8 | Devices support: 9 | - tested on Xiaomi MiWiFi Mini Router with Padavan 3.4.3.9-099_195eba6 10 | - [reported](https://github.com/PaulAnnekov/home-assistant-padavan-tracker/issues/11) working on Asus N56U Router with Padavan 3.4.3.9-099 11 | 12 | Probably need additional changes to make it work on other devices. 13 | 14 | Purpose 15 | ------- 16 | 17 | Detect ANY Wi-Fi clients (=Android/iOS/Windows Phone smartphones...) with 100% accuracy at any time moment. 18 | 19 | Why not ...? 20 | ------------ 21 | 22 | - [Nmap](https://home-assistant.io/components/device_tracker.nmap_tracker/) - mobile devices (Nexus 5X, iPhones) can 23 | go to a deep sleep so nmap can send dozen different packages and get nothing. It's very unreliable. You need at 24 | least 3 minutes to understand client is really offline and not ignoring your requests. 25 | - [OpenWrt luci](https://home-assistant.io/components/device_tracker.luci/) - can't check, but from [source code](https://github.com/home-assistant/home-assistant/blob/dev/homeassistant/components/device_tracker/luci.py#L101) 26 | it checks ARP table which is totally wrong, because it doesn't remove client immediately after disconnect. 27 | - [OpenWrt ubus](https://home-assistant.io/components/device_tracker.ubus/) - looks promising, but doesn't exist in 28 | Padavan firmware out of the box. 29 | - [Xiaomi](https://home-assistant.io/components/device_tracker.xiaomi/) - works like this solution (=perfectly), 30 | but only in _router_ mode. Padavan tracker works in AP mode too. 31 | 32 | Installation (Xiaomi MiWiFi Mini Router only) 33 | ------------------------------------------ 34 | 35 | 1. Download stock Xiaomi dev firmware http://www1.miwifi.com/miwifi_download.html. 36 | 2. Flash it via web interface. 37 | 3. Install Android app ([ru](https://4pda.ru/forum/index.php?showtopic=661224), 38 | [en](http://xiaomi.eu/community/threads/xiaomi-router-app-translation.25386/page-3#post-262621)). 39 | 4. Attach router to your Mi account. 40 | 5. Download ssh unlock firmware http://d.miwifi.com/rom/ssh, remember login/pass - it's ssh credentials. 41 | 6. Put it on USB FAT32 stick: 42 | 1. Turn on Router while reset-button pressed and USB stick plugged in 43 | 2. Release Reset-button after the orange LED starts flashing 44 | 3. Wait a minute to complete flashing and device is online again (shown by blue LED) 45 | 7. Check SSH to your device. 46 | 8. Go to http://prometheus.freize.net/index.html: 47 | 1. Download utility. 48 | 2. Build Toolchain. 49 | 3. Build Firmware. 50 | 4. Flash Firmware. 51 | 5. Flash EEPROM. 52 | 9. Add the following lines to the `configuration.yaml`: 53 | 54 | ```yaml 55 | device_tracker: 56 | - platform: padavan_tracker 57 | consider_home: 10 58 | interval_seconds: 3 59 | url: http://192.168.1.1/ # web interface url (don't forget about `/` in the end) 60 | username: admin # Web interface user name 61 | password: admin # Web interface user pass 62 | ``` 63 | 64 | Notes 65 | ----- 66 | 67 | - Sometimes/most of the time web interface will be inaccessible while this component is working. That's because Padavan firmware doesn't allow >1 users authorized from different IPs. Check the possible [workaround](https://github.com/PaulAnnekov/home-assistant-padavan-tracker/issues/8) for this. 68 | 69 | 70 | Useful links 71 | ------------- 72 | 73 | - Firmware sources: https://bitbucket.org/padavan/rt-n56u 74 | - Firmware build and installation utility: http://prometheus.freize.net/index.html 75 | - OpenWrt wiki related to Xiaomi MiWiFi Mini: https://wiki.openwrt.org/toh/xiaomi/mini 76 | -------------------------------------------------------------------------------- /custom_components/padavan_tracker/device_tracker.py: -------------------------------------------------------------------------------- 1 | """ 2 | Support for Padavan-firmware routers. 3 | """ 4 | import logging 5 | import voluptuous as vol 6 | import re 7 | import homeassistant.helpers.config_validation as cv 8 | from homeassistant.components.device_tracker import ( 9 | DOMAIN, PLATFORM_SCHEMA, CONF_SCAN_INTERVAL, DeviceScanner) 10 | from homeassistant.const import CONF_URL, CONF_PASSWORD, CONF_USERNAME 11 | 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | CONF_RSSI = 'rssi' 15 | 16 | PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ 17 | vol.Optional(CONF_URL, default='http://192.168.1.1/'): cv.string, 18 | vol.Optional(CONF_USERNAME, default='admin'): cv.string, 19 | vol.Optional(CONF_PASSWORD, default='admin'): cv.string, 20 | vol.Optional(CONF_RSSI): vol.All(vol.Coerce(int), vol.Range(min=-200, max=0)), 21 | }) 22 | 23 | 24 | def get_scanner(hass, config): 25 | """Validate the configuration and return PadavanDeviceScanner.""" 26 | _LOGGER.debug('Padavan init') 27 | scanner = PadavanDeviceScanner(config[DOMAIN]) 28 | return scanner if scanner.success_init else None 29 | 30 | 31 | class PadavanDeviceScanner(DeviceScanner): 32 | """This class queries a Padavan-based router.""" 33 | 34 | def __init__(self, config): 35 | """Initialize the scanner.""" 36 | self.last_results = None 37 | self.url = config[CONF_URL] 38 | self.username = config[CONF_USERNAME] 39 | self.password = config[CONF_PASSWORD] 40 | self.rssi_min = config.get(CONF_RSSI) 41 | self.scan_interval = config[CONF_SCAN_INTERVAL] 42 | self.last_results = [] 43 | 44 | # NOTE: Padavan httpd will even don't check HTTP authorization header if multiple devices connected, if will 45 | # show "You cannot Login unless logout another user first." page instead with mac/ip of authorized device. 46 | r = self._request() 47 | self.success_init = True if 'text' in r or r['error_id'] == 'multiple' else False 48 | 49 | if self.success_init: 50 | _LOGGER.info('Successfully connected to Padavan-based router') 51 | if 'error_id' in r: 52 | _LOGGER.info('But %s', r['error_msg']) 53 | else: 54 | _LOGGER.error('Failed to connect to Padavan-based router: %s', r['error_msg']) 55 | 56 | def scan_devices(self): 57 | self._update_info() 58 | _LOGGER.debug('active_hosts %s', str(self.last_results)) 59 | return self.last_results 60 | 61 | def get_device_name(self, mac): 62 | return None 63 | 64 | def _request(self, path='', timeout=5): 65 | import requests 66 | from requests.auth import HTTPBasicAuth 67 | from requests.exceptions import HTTPError, ConnectionError, RequestException 68 | 69 | error_id = None 70 | error_msg = None 71 | r = None 72 | 73 | try: 74 | r = requests.get( 75 | self.url + path, 76 | auth=HTTPBasicAuth(self.username, self.password), 77 | timeout=timeout) 78 | r.raise_for_status() 79 | except HTTPError as e: 80 | error_id = 'status' 81 | error_msg = 'Bad status: ' + str(e) 82 | except ConnectionError as e: 83 | error_id = 'connection' 84 | error_msg = "Can't connect to router: " + str(e) 85 | except RequestException as e: 86 | error_id = 'other' 87 | error_msg = 'Some error during request: ' + str(e) 88 | 89 | if not error_id: 90 | if r.headers['Server'] is None or r.headers['Server'] != 'httpd': 91 | error_id = 'not_padavan' 92 | error_msg = "Router's firmware doesn't look like Padavan. 'Server' HTTP header should be 'httpd'" 93 | if '' in r.text: 94 | m = re.search("'((\d{1,3}\.)+\d{1,3})'.*'((\w{2}:)+\w{2})'", r.text, re.S) 95 | device = m.group(1)+'/'+m.group(3) if m else 'IP unavailable' 96 | error_id = 'multiple' 97 | error_msg = "There are multiple connections to web interface ({}). Can't query data".format(device) 98 | 99 | return {'error_id': error_id, 'error_msg': error_msg} if error_id else {'text': r.text} 100 | 101 | def _update_info(self): 102 | """Retrieve latest information from the router.""" 103 | _LOGGER.debug('Polling') 104 | 105 | timeout = int(self.scan_interval.total_seconds()/2) 106 | r_2g = self._request('Main_WStatus2g_Content.asp', timeout) 107 | r_5g = self._request('Main_WStatus_Content.asp', timeout) 108 | if 'error_id' in r_2g or 'error_id' in r_5g: 109 | _LOGGER.error("Can't get connected clients: %s", r_2g['error_msg'] if 'error_msg' in r_2g else 110 | r_5g['error_msg']) 111 | return 112 | 113 | self.last_results = [] 114 | debug = [] 115 | both = r_2g['text'] + r_5g['text'] 116 | for line in both.split('\n'): 117 | m = re.match("^((.{2}:){5}.{2}) ", line) 118 | if m: 119 | values = line.split() 120 | rssi = int(values[8]) 121 | debug.append({'mac': values[0], 'rssi': rssi, 'psm': values[9], 'time': values[10], 122 | 'bw': values[2], 'mcs': values[3], }) 123 | if self.rssi_min and rssi < self.rssi_min: 124 | continue 125 | self.last_results.append(m.group(1)) 126 | 127 | _LOGGER.debug('results %s', str(debug)) 128 | 129 | return 130 | --------------------------------------------------------------------------------