From 13ec33204ea080b0869b82f9e5cd6750ecedfc47 Mon Sep 17 00:00:00 2001 From: Dmitry Mamontov Date: Sat, 16 Apr 2022 00:02:36 +0300 Subject: [PATCH] Add diagnostics --- README.md | 24 +++++++++ custom_components/miwifi/const.py | 5 ++ custom_components/miwifi/diagnostics.py | 57 +++++++++++++++++++++ custom_components/miwifi/luci.py | 66 +++++++++++++++++++------ custom_components/miwifi/manifest.json | 2 +- 5 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 custom_components/miwifi/diagnostics.py diff --git a/README.md b/README.md index a53b859..4b38d83 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Component for tracking devices and managing routers based on [MiWiFi](http://miw - [Supported routers](#supported-routers) - [API check list](#api-check-list) - [Summary](#summary) +- [Diagnostics](#diagnostics) ## FAQ **Q. Do I need to get telnet or ssh?** @@ -163,3 +164,26 @@ Many more Xiaomi and Redmi routers supported by MiWiFi (OpenWRT - Luci API) | ![](images/R1C.png) | **Mi Router Mini** | R1CM | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢 | | ![](images/R2D.png) | **Mi Router R2D** | R2D | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢 | | ![](images/R1D.png) | **Mi Router R1D** | R1D | 🟢🟢🟢🟢 | 🟢🟢🟢🟢🟢🟢🟢🟢🔴 | 🟢🟢🟢🟢 | + +## Diagnostics +You will need to obtain diagnostic data to search for a problem or before creating an issue. + +### Via GUI: + +How to obtain diagnostic data can be found on the website HASS: [Diagnostic Documentation](https://www.home-assistant.io/integrations/diagnostics/) + +### Via Debug: + +❗ Check the data that you publish in the issue, they may contain secret data. + +Set component to debug mode and reload HASS: + +```yaml +logger: + default: error + logs: + ... + custom_components.miwifi: debug +``` + +Then wait a bit and you can watch the logs, they will need information \ No newline at end of file diff --git a/custom_components/miwifi/const.py b/custom_components/miwifi/const.py index 3475b45..a2c33cf 100644 --- a/custom_components/miwifi/const.py +++ b/custom_components/miwifi/const.py @@ -25,6 +25,11 @@ DISCOVERY: Final = "discovery" DISCOVERY_INTERVAL: Final = timedelta(minutes=60) +"""Diagnostic const""" +DIAGNOSTIC_DATE_TIME: Final = "date_time" +DIAGNOSTIC_MESSAGE: Final = "message" +DIAGNOSTIC_CONTENT: Final = "content" + """Helper const""" UPDATER: Final = "updater" UPDATE_LISTENER: Final = "update_listener" diff --git a/custom_components/miwifi/diagnostics.py b/custom_components/miwifi/diagnostics.py new file mode 100644 index 0000000..9387ccc --- /dev/null +++ b/custom_components/miwifi/diagnostics.py @@ -0,0 +1,57 @@ +"""MiWifi diagnostic.""" + +from __future__ import annotations + +from typing import Final + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + CONF_URL, + CONF_TOKEN, + CONF_ID, +) +from homeassistant.core import HomeAssistant + +from .const import ( + DOMAIN, + UPDATER, + ATTR_CAMERA_IMAGE, +) + +TO_REDACT: Final = { + CONF_PASSWORD, + CONF_USERNAME, + CONF_URL, + CONF_TOKEN, + CONF_ID, + ATTR_CAMERA_IMAGE, + "routerId", + "gateWay", + "hostname", + "ipv4", + "ssid", + "pwd", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict: + """Return diagnostics for a config entry.""" + + _data: dict = {"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT)} + + if _updater := hass.data[DOMAIN][config_entry.entry_id].get(UPDATER): + if hasattr(_updater, "data"): + _data["data"] = async_redact_data(_updater.data, TO_REDACT) + + if hasattr(_updater, "devices"): + _data["devices"] = _updater.devices + + if len(_updater.luci.diagnostics) > 0: + _data["requests"] = async_redact_data(_updater.luci.diagnostics, TO_REDACT) + + return _data diff --git a/custom_components/miwifi/luci.py b/custom_components/miwifi/luci.py index 223ead3..2140f94 100644 --- a/custom_components/miwifi/luci.py +++ b/custom_components/miwifi/luci.py @@ -11,6 +11,8 @@ import uuid import urllib.parse +from datetime import datetime +from typing import Any from httpx import AsyncClient, Response, HTTPError from homeassistant.util import slugify @@ -23,6 +25,9 @@ CLIENT_LOGIN_TYPE, CLIENT_NONCE_TYPE, CLIENT_PUBLIC_KEY, + DIAGNOSTIC_DATE_TIME, + DIAGNOSTIC_MESSAGE, + DIAGNOSTIC_CONTENT, ) from .exceptions import LuciConnectionException, LuciTokenException @@ -67,14 +72,17 @@ def __init__( self._url = CLIENT_URL.format(ip=ip) + self.diagnostics: dict[str, Any] = {} + async def login(self) -> dict: """Login method :return dict: dict with login data. """ + _method: str = "xqsystem/login" _nonce: str = self.generate_nonce() - _url: str = f"{self._url}/api/xqsystem/login" + _url: str = f"{self._url}/api/{_method}" try: async with self._client as client: @@ -91,15 +99,17 @@ async def login(self) -> dict: timeout=self._timeout, ) - _LOGGER.debug("Successful request %s: %s", _url, response.content) + self._debug("Successful request", _url, response.content, _method) _data: dict = json.loads(response.content) except (HTTPError, ValueError, TypeError) as _e: - _LOGGER.debug("Connection error %r", _e) + self._debug("Connection error", _url, _e, _method) raise LuciConnectionException("Connection error") from _e if response.status_code != 200 or "token" not in _data: + self._debug("Failed to get token", _url, _data, _method) + raise LuciTokenException("Failed to get token") self._token = _data["token"] @@ -112,15 +122,16 @@ async def logout(self) -> None: if self._token is None: return - _url: str = f"{self._url}/;stok={self._token}/web/logout" + _method: str = "logout" + _url: str = f"{self._url}/;stok={self._token}/web/{_method}" try: async with self._client as client: response: Response = await client.get(_url, timeout=self._timeout) - _LOGGER.debug("Successful request %s: %s", _url, response.content) + self._debug("Successful request", _url, response.content, _method) except (HTTPError, ValueError, TypeError) as _e: - _LOGGER.debug("Logout error: %r", _e) + self._debug("Logout error", _url, _e, _method) async def get( self, path: str, query_params: dict | None = None, use_stok: bool = True @@ -143,15 +154,17 @@ async def get( async with self._client as client: response: Response = await client.get(_url, timeout=self._timeout) - _LOGGER.debug("Successful request %s: %s", _url, response.content) + self._debug("Successful request", _url, response.content, path) _data: dict = json.loads(response.content) except (HTTPError, ValueError, TypeError) as _e: - _LOGGER.debug("Connection error %r", _e) + self._debug("Connection error", _url, _e, path) raise LuciConnectionException("Connection error") from _e if "code" not in _data or _data["code"] > 0: + self._debug("Invalid error code received", _url, _data, path) + raise LuciTokenException("Invalid error code received") return _data @@ -308,20 +321,19 @@ async def image(self, hardware: str) -> bytes | None: """ hardware = slugify(hardware.lower()) - # fmt: off - url: str = f"http://{self.ip}/xiaoqiang/web/img/icons/router_{hardware}_100_on.png" - # fmt: on + _path: str = f"icons/router_{hardware}_100_on.png" + _url: str = f"http://{self.ip}/xiaoqiang/web/img/{_path}" try: async with self._client as client: - response: Response = await client.get(url, timeout=self._timeout) + response: Response = await client.get(_url, timeout=self._timeout) - _LOGGER.debug("Successful request image %s", url) + self._debug("Successful request image", _url, response.status_code, _path) if len(response.content) > 0: return base64.b64encode(response.content) - except HTTPError: - return None + except HTTPError as _e: + self._debug("Error request image", _url, _e, _path) return None @@ -365,3 +377,27 @@ def generate_password_hash(self, nonce: str, password: str) -> str: """ return self.sha1(nonce + self.sha1(password + CLIENT_PUBLIC_KEY)) + + def _debug(self, message: str, url: str, content: Any, path: str) -> None: + """Debug log + + :param message: str: Message + :param url: str: URL + :param content: Any: Content + :param path: str: Path + """ + + _LOGGER.debug("%s (%s): %s", message, url, str(content)) + + _content: dict | str = {} + + try: + _content = json.loads(content) + except (ValueError, TypeError): + _content = str(content) + + self.diagnostics[path] = { + DIAGNOSTIC_DATE_TIME: datetime.now().replace(microsecond=0).isoformat(), + DIAGNOSTIC_MESSAGE: message, + DIAGNOSTIC_CONTENT: _content, + } diff --git a/custom_components/miwifi/manifest.json b/custom_components/miwifi/manifest.json index dca5bde..55cd609 100644 --- a/custom_components/miwifi/manifest.json +++ b/custom_components/miwifi/manifest.json @@ -1,7 +1,7 @@ { "domain": "miwifi", "name": "MiWiFi", - "version": "2.3.1", + "version": "2.4.0", "documentation": "https://github.com/dmamontov/hass-miwifi/blob/main/README.md", "issue_tracker": "https://github.com/dmamontov/hass-miwifi/issues", "config_flow": true,