Tenda Router Device Tracker

Hi,

I have a problem with the device tracker for the Tenda router. The actual integration is from the HACS Store and for the Tenda AC23 router. It also works with my Tenda RX3 router. However, the integration uses /goform/getOnlineList? to query the connected devices. But this is the list for the known devices, not for the currently available ones. As the devices are known to the router, they are always displayed as online/present.
You can get the actual list of available devices via /goform/GetIpMacBind?
There you get the following list:

{"lanIp":"192.168.1.35","lanMask":"255.255.255.0","dhttpIP":"0.0.0.0","dhcpClientList":[{"ipaddr":"192.168.1.229","macaddr":"01:02:03:04:05:06","devname":"homeassistant","status":"1"}, {"ipaddr":"192.168.1.101","macaddr":"01:02:03:04:05:06","devname":"AMD-Ryzen5","status":"1"}, {"ipaddr":"192.168.1.125","macaddr":"01:02:03:04:05:06","devname":"HF-LPB130","status":"1"}, {"ipaddr":"192.168.1.107","macaddr":"01:02:03:04:05:06","devname":"Coreelec","status":"1"}, {"ipaddr":"192.168.1.216","macaddr":"01:02:03:04:05:06","devname":"CoreELEC","status":"1"}, {"ipaddr":"192.168.1.238","macaddr":"01:02:03:04:05:06","devname":"pihole","status":"1"}, {"ipaddr":"192.168.1.252","macaddr":"01:02:03:04:05:06","devname":"klipper","status":"1"}, {"ipaddr":"192.168.1.225","macaddr":"01:02:03:04:05:06","devname":"Echo Flex","status":"1"}, {"ipaddr":"192.168.1.139","macaddr":"01:02:03:04:05:06","devname":"HF-LPB130","status":"1"}],"bindList":[{"ipaddr":"192.168.1.142","macaddr":"01:02:03:04:05:06","devname":"iPhone","status":"1"}, {"ipaddr":"192.168.1.165","macaddr":"01:02:03:04:05:06","devname":"Mi9TPro","status":"1"}, {"ipaddr":"192.168.1.201","macaddr":"01:02:03:04:05:06","devname":"Lampe","status":"1"}, {"ipaddr":"192.168.1.202","macaddr":"01:02:03:04:05:06","devname":"Steckdose","status":"1"}, {"ipaddr":"192.168.1.200","macaddr":"01:02:03:04:05:06","devname":"Licht","status":"1"}, {"ipaddr":"192.168.1.108","macaddr":"01:02:03:04:05:06","devname":"DS216j","status":"1"}]}

“status” 0/1 indicates whether the known device is really online.
I have tried to adapt the actual script, but without success. The devices are always shown as absent.

import hashlib
import json
import logging
from time import time
import homeassistant.helpers.config_validation as cv
import requests
import voluptuous as vol
from homeassistant.components.device_tracker import (
    DOMAIN,
    PLATFORM_SCHEMA,
    DeviceScanner,
)
from homeassistant.const import (
    CONF_HOST,
    CONF_PASSWORD,
)

_LOGGER = logging.getLogger(__name__)

HTTP_HEADER_NO_CACHE = "no-cache"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_HOST): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
    }
)


def get_scanner(hass, config):
    scanner = TendaDeviceScanner(config[DOMAIN])
    if scanner.is_initialized:
        return scanner
    return None


class TendaDeviceScanner(DeviceScanner):
    def __init__(self, config):
        host = config[CONF_HOST]
        password = config[CONF_PASSWORD]
        self.is_initialized = False
        self.last_results = {}

        try:
            self.tenda_client = TendaClient(host, password)
            self._update_info()
            self.is_initialized = True
        except requests.exceptions.ConnectionError:
            _LOGGER.error("Cannot connect to tenda device")

    def scan_devices(self):
        _LOGGER.debug("Scanning devices...")
        self._update_info()
        return self.last_results

    def get_device_name(self, device):
        return self.last_results.get(device)

    def _update_info(self):
        _LOGGER.debug("Loading wireless clients...")
        self.last_results = self.tenda_client.get_connected_devices()


class TendaClient:
    def __init__(self, host: str, password: str) -> None:
        self.host = host
        self.password = password
        self.cookies = None
        self.is_authorized = None

    def auth(self):
        _LOGGER.debug("Trying to authorize")
        headers = {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        }

        data = (
                "username=admin&password=" + hashlib.md5(self.password.encode()).hexdigest()
        )
        response = requests.post(
            "http://" + self.host + "/login/Auth",
            headers=headers,
            data=data,
            verify=False,
            allow_redirects=False,
        )
        self.cookies = response.cookies

    def get_connected_devices(self):
        if self.cookies is None:
            _LOGGER.debug("Cookies not found")
            self.auth()

        response = requests.get(
            "http://" + self.host + "/goform/GetIpMacBind?" + str(time()),
            verify=False,
            cookies=self.cookies,
            allow_redirects=False,
        )

        try:
            json_response = json.loads(response.content)
        except json.JSONDecodeError:
            self.cookies = None
            return self.get_connected_devices()

        devices = {}

        for device in json_response("bindList"):
            mac = None
            devname = None
            status = None

            if "macaddr" in device:
                mac = device.get("macaddr")

            if "devname" in device:
                devname = device.get("devname")

            if "status" in device:
                status = device.get("status")

            if mac is not None and devname is not None and status == "1":
                devices[mac] = devname

        return devices

Could someone help me with this? Unfortunately I am not a software developer.
I’m glad that I created this working standalone Python script to show me the available devices.

import requests
import hashlib
from time import time
import json

def get_connected_devices(host, password):
    """
    Ermittelt eine Liste der verbundenen Geräte an einem Tenda-Router.

    Args:
        host: Die IP-Adresse des Tenda-Routers.
        password: Das Passwort des Tenda-Routers.

    Returns:
        Eine Liste der verbundenen Geräte.
    """

    # Authentifiziere dich beim Router.

    headers = {
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    }

    data = (
        "username=admin&password=" + hashlib.md5(password.encode()).hexdigest()
    )
    response = requests.post(
        "http://" + host + "/login/Auth",
        headers=headers,
        data=data,
        verify=False,
        allow_redirects=False,
    )
    response = requests.post(
        "http://" + host + "/login/Auth",
        headers=headers,
        data=data,
        verify=False,
        allow_redirects=False,
    )

    # Speichere die Cookies.

    cookies = response.cookies

    # Erhalte die Liste der verbundenen Geräte.

    response = requests.get(
        "http://" + host + "/goform/GetIpMacBind?" + str(time()),
        verify=False,
        cookies=cookies,
        allow_redirects=False,
    )

    # Dekodiere die Antwort.

    try:
        json_response = json.loads(response.content)
    except json.JSONDecodeError:
        return {}

    # Gib die Liste der verbundenen Geräte zurück.

    return json_response["bindList"]


if __name__ == "__main__":
    # Gib die Liste der verbundenen Geräte aus.

    host = "192.168.1.35"
    password = "password"
    devices = get_connected_devices(host, password)

    for device in devices:
        if device["status"] == "1":
            print("Gerät: %s - MAC: %s" % (device["devname"],device["macaddr"]))

That should probably be reported As a bug on the git repo you posted. That way the developer can fix it.

Go here

Then click issues (near the top) then New Issue (green button) and answer the questions (they’ll want what you collected above)

I did, but unfortunately he wrote… “Sorry, but I don’t have this router now. So I can’t test it :frowning:.”

You can fork the custom integration, change only interested file and install your own fork over HACS.

And why not ping based device tracker?

That’s what I’m trying to do, but it’s not working. I have already adjusted the script accordingly, but the devices are displayed as offline/absent.
There’s still a mistake somewhere or I’m missing something. Or maybe I’m completely wrong.

I actually managed to do it myself, but had to switch on debug logging to detect the problems.
Now the Tenda plugin works and shows all devices with “status”= 1 in the “bindList”.

import hashlib
import json
import logging
from time import time
import homeassistant.helpers.config_validation as cv
import requests
import voluptuous as vol
from homeassistant.components.device_tracker import (
    DOMAIN,
    PLATFORM_SCHEMA,
    DeviceScanner,
)
from homeassistant.const import (
    CONF_HOST,
    CONF_PASSWORD,
)

_LOGGER = logging.getLogger(__name__)

HTTP_HEADER_NO_CACHE = "no-cache"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_HOST): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
    }
)


def get_scanner(hass, config):
    scanner = TendaDeviceScanner(config[DOMAIN])
    if scanner.is_initialized:
        return scanner
    return None


class TendaDeviceScanner(DeviceScanner):
    def __init__(self, config):
        host = config[CONF_HOST]
        password = config[CONF_PASSWORD]
        self.is_initialized = False
        self.last_results = {}

        try:
            self.tenda_client = TendaClient(host, password)
            self._update_info()
            self.is_initialized = True
        except requests.exceptions.ConnectionError:
            _LOGGER.error("Cannot connect to tenda device")

    def scan_devices(self):
        _LOGGER.debug("Scanning devices...")
        self._update_info()
        return self.last_results

    def get_device_name(self, device):
        return self.last_results.get(device)

    def _update_info(self):
        _LOGGER.debug("Loading wireless clients...")
        self.last_results = self.tenda_client.get_connected_devices()


class TendaClient:
    def __init__(self, host: str, password: str) -> None:
        self.host = host
        self.password = password
        self.cookies = None
        self.is_authorized = None

    def auth(self):
        _LOGGER.debug("Trying to authorize")
        headers = {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        }

        data = (
                "username=admin&password=" + hashlib.md5(self.password.encode()).hexdigest()
        )
        response = requests.post(
            "http://" + self.host + "/login/Auth",
            headers=headers,
            data=data,
            verify=False,
            allow_redirects=False,
        )
        self.cookies = response.cookies
        self.cookies = response.cookies

    def get_connected_devices(self):
        if self.cookies is None:
            _LOGGER.debug("Cookies not found")
            self.auth()

        response = requests.get(
            "http://" + self.host + "/goform/GetIpMacBind?" + str(time()),
            verify=False,
            cookies=self.cookies,
            allow_redirects=False,
        )

        try:
            json_response = json.loads(response.content)
        except json.JSONDecodeError:
            self.cookies = None
            return self.get_connected_devices()

        devices = {}

        for device in json_response["bindList"]:
            mac = None
            name = None
            status = None
            _LOGGER.debug(device)
            if "macaddr" in device:
                mac = device.get("macaddr")

            if "devname" in device:
                name = device.get("devname")

            if "status" in device:
                status = device.get("status")

            if status == "1":
                devices[mac] = name

        return devices

I conccur this work… many thanks man…

I also added searching dhcpClientlist since it wont find devices that have their IP reserved in dhcpreservation… below is the modified content of device_tracker.py… for anyone who needs this working… tenda rx3…

import hashlib
import json
import logging
from time import time
import homeassistant.helpers.config_validation as cv
import requests
import voluptuous as vol
from homeassistant.components.device_tracker import (
    DOMAIN,
    PLATFORM_SCHEMA,
    DeviceScanner,
)
from homeassistant.const import (
    CONF_HOST,
    CONF_PASSWORD,
)

_LOGGER = logging.getLogger(__name__)

HTTP_HEADER_NO_CACHE = "no-cache"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
    {
        vol.Required(CONF_HOST): cv.string,
        vol.Required(CONF_PASSWORD): cv.string,
    }
)


def get_scanner(hass, config):
    scanner = TendaDeviceScanner(config[DOMAIN])
    if scanner.is_initialized:
        return scanner
    return None


class TendaDeviceScanner(DeviceScanner):
    def __init__(self, config):
        host = config[CONF_HOST]
        password = config[CONF_PASSWORD]
        self.is_initialized = False
        self.last_results = {}

        try:
            self.tenda_client = TendaClient(host, password)
            self._update_info()
            self.is_initialized = True
        except requests.exceptions.ConnectionError:
            _LOGGER.error("Cannot connect to tenda device")

    def scan_devices(self):
        _LOGGER.debug("Scanning devices...")
        self._update_info()
        return self.last_results

    def get_device_name(self, device):
        return self.last_results.get(device)

    def _update_info(self):
        _LOGGER.debug("Loading wireless clients...")
        self.last_results = self.tenda_client.get_connected_devices()


class TendaClient:
    def __init__(self, host: str, password: str) -> None:
        self.host = host
        self.password = password
        self.cookies = None
        self.is_authorized = None

    def auth(self):
        _LOGGER.debug("Trying to authorize")
        headers = {
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        }

        data = (
                "username=admin&password=" + hashlib.md5(self.password.encode()).hexdigest()
        )
        response = requests.post(
            "http://" + self.host + "/login/Auth",
            headers=headers,
            data=data,
            verify=False,
            allow_redirects=False,
        )
        self.cookies = response.cookies

    def get_connected_devices(self):
        if self.cookies is None:
            _LOGGER.debug("Cookies not found")
            self.auth()

        response = requests.get(
            "http://" + self.host + "/goform/GetIpMacBind?" + str(time()),
            verify=False,
            cookies=self.cookies,
            allow_redirects=False,
        )

        try:
            json_response = json.loads(response.content)
        except json.JSONDecodeError:
            self.cookies = None
            return self.get_connected_devices()

        devices = {}

        for device in json_response["dhcpClientList"]:
            mac = None
            name = None
            status = None
            _LOGGER.debug(device)
            if "macaddr" in device:
                mac = device.get("macaddr")

            if "devname" in device:
                name = device.get("devname")

            if "status" in device:
                status = device.get("status")

            if status == "1":
                devices[mac] = name
        
        for device in json_response["bindList"]:
            mac = None
            name = None
            status = None
            _LOGGER.debug(device)
            if "macaddr" in device:
                mac = device.get("macaddr")

            if "devname" in device:
                name = device.get("devname")

            if "status" in device:
                status = device.get("status")

            if status == "1":
                devices[mac] = name

        return devices

How would one go about easily installing this updated version? is there a github repo to add to HACS or similar?

Here I had to fork the original repository and add the fork link as a custom repository.

Do you use this data in a card to show connected devices?