From fd7e2e76e7d17b2811eac0519b4f672e510d4f6e Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 6 Feb 2022 23:50:44 +0100 Subject: [PATCH] Add tplink diagnostics (#65822) --- .../components/tplink/diagnostics.py | 46 +++++++ tests/components/tplink/__init__.py | 28 ++++- tests/components/tplink/consts.py | 116 ------------------ .../tplink-diagnostics-data-bulb-kl130.json | 108 ++++++++++++++++ .../tplink-diagnostics-data-plug-hs110.json | 74 +++++++++++ tests/components/tplink/test_diagnostics.py | 60 +++++++++ 6 files changed, 315 insertions(+), 117 deletions(-) create mode 100644 homeassistant/components/tplink/diagnostics.py delete mode 100644 tests/components/tplink/consts.py create mode 100644 tests/components/tplink/fixtures/tplink-diagnostics-data-bulb-kl130.json create mode 100644 tests/components/tplink/fixtures/tplink-diagnostics-data-plug-hs110.json create mode 100644 tests/components/tplink/test_diagnostics.py diff --git a/homeassistant/components/tplink/diagnostics.py b/homeassistant/components/tplink/diagnostics.py new file mode 100644 index 00000000000..5771bee5bd3 --- /dev/null +++ b/homeassistant/components/tplink/diagnostics.py @@ -0,0 +1,46 @@ +"""Diagnostics support for TPLink.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import TPLinkDataUpdateCoordinator + +TO_REDACT = { + # Entry fields + "unique_id", # based on mac address + # Device identifiers + "alias", + "mac", + "mic_mac", + "host", + "hwId", + "oemId", + "deviceId", + # Device location + "latitude", + "latitude_i", + "longitude", + "longitude_i", + # Cloud connectivity info + "username", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: TPLinkDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + device = coordinator.device + + data = {} + data[ + "device_last_response" + ] = device._last_update # pylint: disable=protected-access + + return async_redact_data(data, TO_REDACT) diff --git a/tests/components/tplink/__init__.py b/tests/components/tplink/__init__.py index 02b7404201a..beeaa21bf27 100644 --- a/tests/components/tplink/__init__.py +++ b/tests/components/tplink/__init__.py @@ -2,10 +2,16 @@ from unittest.mock import AsyncMock, MagicMock, patch -from kasa import SmartBulb, SmartDimmer, SmartPlug, SmartStrip +from kasa import SmartBulb, SmartDevice, SmartDimmer, SmartPlug, SmartStrip from kasa.exceptions import SmartDeviceException from kasa.protocol import TPLinkSmartHomeProtocol +from homeassistant.components.tplink import CONF_HOST +from homeassistant.components.tplink.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + MODULE = "homeassistant.components.tplink" MODULE_CONFIG_FLOW = "homeassistant.components.tplink.config_flow" IP_ADDRESS = "127.0.0.1" @@ -146,3 +152,23 @@ def _patch_single_discovery(device=None, no_device=False): return patch( "homeassistant.components.tplink.Discover.discover_single", new=_discover_single ) + + +async def initialize_config_entry_for_device( + hass: HomeAssistant, dev: SmartDevice +) -> MockConfigEntry: + """Create a mocked configuration entry for the given device. + + Note, the rest of the tests should probably be converted over to use this + instead of repeating the initialization routine for each test separately + """ + config_entry = MockConfigEntry( + title="TP-Link", domain=DOMAIN, unique_id=dev.mac, data={CONF_HOST: dev.host} + ) + config_entry.add_to_hass(hass) + + with _patch_discovery(device=dev), _patch_single_discovery(device=dev): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/tplink/consts.py b/tests/components/tplink/consts.py deleted file mode 100644 index e579be61df2..00000000000 --- a/tests/components/tplink/consts.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Constants for the TP-Link component tests.""" - -SMARTPLUG_HS110_DATA = { - "sysinfo": { - "sw_ver": "1.0.4 Build 191111 Rel.143500", - "hw_ver": "4.0", - "model": "HS110(EU)", - "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", - "oemId": "40F54B43071E9436B6395611E9D91CEA", - "hwId": "A6C77E4FDD238B53D824AC8DA361F043", - "rssi": -24, - "longitude_i": 130793, - "latitude_i": 480582, - "alias": "SmartPlug", - "status": "new", - "mic_type": "IOT.SMARTPLUGSWITCH", - "feature": "TIM:ENE", - "mac": "69:F2:3C:8E:E3:47", - "updating": 0, - "led_off": 0, - "relay_state": 0, - "on_time": 0, - "active_mode": "none", - "icon_hash": "", - "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", - "next_action": {"type": -1}, - "err_code": 0, - }, - "realtime": { - "voltage_mv": 233957, - "current_ma": 21, - "power_mw": 0, - "total_wh": 1793, - "err_code": 0, - }, -} -SMARTPLUG_HS100_DATA = { - "sysinfo": { - "sw_ver": "1.0.4 Build 191111 Rel.143500", - "hw_ver": "4.0", - "model": "HS100(EU)", - "deviceId": "4C56447B395BB7A2FAC68C9DFEE2E84163222581", - "oemId": "40F54B43071E9436B6395611E9D91CEA", - "hwId": "A6C77E4FDD238B53D824AC8DA361F043", - "rssi": -24, - "longitude_i": 130793, - "latitude_i": 480582, - "alias": "SmartPlug", - "status": "new", - "mic_type": "IOT.SMARTPLUGSWITCH", - "feature": "TIM:", - "mac": "A9:F4:3D:A4:E3:47", - "updating": 0, - "led_off": 0, - "relay_state": 0, - "on_time": 0, - "active_mode": "none", - "icon_hash": "", - "dev_name": "Smart Wi-Fi Plug", - "next_action": {"type": -1}, - "err_code": 0, - } -} -SMARTSTRIP_KP303_DATA = { - "sysinfo": { - "sw_ver": "1.0.4 Build 210428 Rel.135415", - "hw_ver": "1.0", - "model": "KP303(AU)", - "deviceId": "03102547AB1A57A4E4AA5B4EFE34C3005726B97D", - "oemId": "1F950FC9BFF278D9D35E046C129D9411", - "hwId": "9E86D4F840D2787D3D7A6523A731BA2C", - "rssi": -74, - "longitude_i": 1158985, - "latitude_i": -319172, - "alias": "TP-LINK_Power Strip_00B1", - "status": "new", - "mic_type": "IOT.SMARTPLUGSWITCH", - "feature": "TIM", - "mac": "D4:DD:D6:95:B0:F9", - "updating": 0, - "led_off": 0, - "children": [ - { - "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913000", - "state": 0, - "alias": "R-Plug 1", - "on_time": 0, - "next_action": {"type": -1}, - }, - { - "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913001", - "state": 1, - "alias": "R-Plug 2", - "on_time": 93835, - "next_action": {"type": -1}, - }, - { - "id": "8006B399B7FE68D4E6991CCCEA239C081DFA913002", - "state": 1, - "alias": "R-Plug 3", - "on_time": 93834, - "next_action": {"type": -1}, - }, - ], - "child_num": 3, - "err_code": 0, - }, - "realtime": { - "voltage_mv": 233957, - "current_ma": 21, - "power_mw": 0, - "total_wh": 1793, - "err_code": 0, - }, - "context": "1", -} diff --git a/tests/components/tplink/fixtures/tplink-diagnostics-data-bulb-kl130.json b/tests/components/tplink/fixtures/tplink-diagnostics-data-bulb-kl130.json new file mode 100644 index 00000000000..4e3d4f01f20 --- /dev/null +++ b/tests/components/tplink/fixtures/tplink-diagnostics-data-bulb-kl130.json @@ -0,0 +1,108 @@ +{ + "device_last_response": { + "system": { + "get_sysinfo": { + "sw_ver": "1.8.8 Build 190613 Rel.123436", + "hw_ver": "1.0", + "model": "KL130(EU)", + "description": "Smart Wi-Fi LED Bulb with Color Changing", + "alias": "bedroom light", + "mic_type": "IOT.SMARTBULB", + "dev_state": "normal", + "mic_mac": "aa:bb:cc:dd:ee:ff", + "deviceId": "1234", + "oemId": "1234", + "hwId": "1234", + "is_factory": false, + "disco_ver": "1.0", + "ctrl_protocols": { + "name": "Linkie", + "version": "1.0" + }, + "light_state": { + "on_off": 1, + "mode": "normal", + "hue": 0, + "saturation": 0, + "color_temp": 2500, + "brightness": 58 + }, + "is_dimmable": 1, + "is_color": 1, + "is_variable_color_temp": 1, + "preferred_state": [ + { + "index": 0, + "hue": 0, + "saturation": 0, + "color_temp": 2500, + "brightness": 10 + }, + { + "index": 1, + "hue": 299, + "saturation": 95, + "color_temp": 0, + "brightness": 100 + }, + { + "index": 2, + "hue": 120, + "saturation": 75, + "color_temp": 0, + "brightness": 100 + }, + { + "index": 3, + "hue": 240, + "saturation": 75, + "color_temp": 0, + "brightness": 100 + } + ], + "rssi": -66, + "active_mode": "none", + "heapsize": 334532, + "err_code": 0 + } + }, + "smartlife.iot.common.emeter": { + "get_realtime": { + "power_mw": 6600, + "err_code": 0 + }, + "get_monthstat": { + "month_list": [ + { + "year": 2022, + "month": 1, + "energy_wh": 321 + }, + { + "year": 2022, + "month": 2, + "energy_wh": 321 + } + ], + "err_code": 0 + }, + "get_daystat": { + "day_list": [ + { + "year": 2022, + "month": 2, + "day": 1, + "energy_wh": 123 + }, + { + "year": 2022, + "month": 2, + "day": 2, + "energy_wh": 123 + } + ], + "err_code": 0 + } + } + } +} diff --git a/tests/components/tplink/fixtures/tplink-diagnostics-data-plug-hs110.json b/tests/components/tplink/fixtures/tplink-diagnostics-data-plug-hs110.json new file mode 100644 index 00000000000..13dd14bbdda --- /dev/null +++ b/tests/components/tplink/fixtures/tplink-diagnostics-data-plug-hs110.json @@ -0,0 +1,74 @@ +{ + "device_last_response": { + "system": { + "get_sysinfo": { + "sw_ver": "1.0.4 Build 191111 Rel.143500", + "hw_ver": "4.0", + "model": "HS110(EU)", + "deviceId": "1234", + "oemId": "1234", + "hwId": "1234", + "rssi": -57, + "longitude_i": "0.0", + "latitude_i": "0.0", + "alias": "some plug", + "status": "new", + "mic_type": "IOT.SMARTPLUGSWITCH", + "feature": "TIM:ENE", + "mac": "aa:bb:cc:dd:ee:ff", + "updating": 0, + "led_off": 1, + "relay_state": 1, + "on_time": 254454, + "active_mode": "none", + "icon_hash": "", + "dev_name": "Smart Wi-Fi Plug With Energy Monitoring", + "next_action": { + "type": -1 + }, + "err_code": 0 + } + }, + "emeter": { + "get_realtime": { + "voltage_mv": 230118, + "current_ma": 303, + "power_mw": 28825, + "total_wh": 18313, + "err_code": 0 + }, + "get_monthstat": { + "month_list": [ + { + "year": 2022, + "month": 2, + "energy_wh": 321 + }, + { + "year": 2022, + "month": 1, + "energy_wh": 321 + } + ], + "err_code": 0 + }, + "get_daystat": { + "day_list": [ + { + "year": 2022, + "month": 2, + "day": 1, + "energy_wh": 123 + }, + { + "year": 2022, + "month": 2, + "day": 2, + "energy_wh": 123 + } + ], + "err_code": 0 + } + } + } +} diff --git a/tests/components/tplink/test_diagnostics.py b/tests/components/tplink/test_diagnostics.py new file mode 100644 index 00000000000..d09ea72b70a --- /dev/null +++ b/tests/components/tplink/test_diagnostics.py @@ -0,0 +1,60 @@ +"""Tests for the diagnostics data provided by the TP-Link integration.""" +import json + +from aiohttp import ClientSession +from kasa import SmartDevice +import pytest + +from homeassistant.core import HomeAssistant + +from . import _mocked_bulb, _mocked_plug, initialize_config_entry_for_device + +from tests.common import load_fixture +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +@pytest.mark.parametrize( + "mocked_dev,fixture_file,sysinfo_vars", + [ + ( + _mocked_bulb(), + "tplink-diagnostics-data-bulb-kl130.json", + ["mic_mac", "deviceId", "oemId", "hwId", "alias"], + ), + ( + _mocked_plug(), + "tplink-diagnostics-data-plug-hs110.json", + ["mac", "deviceId", "oemId", "hwId", "alias", "longitude_i", "latitude_i"], + ), + ], +) +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + mocked_dev: SmartDevice, + fixture_file: str, + sysinfo_vars: list[str], +): + """Test diagnostics for config entry.""" + diagnostics_data = json.loads(load_fixture(fixture_file, "tplink")) + + mocked_dev._last_update = diagnostics_data["device_last_response"] + + config_entry = await initialize_config_entry_for_device(hass, mocked_dev) + result = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + + assert isinstance(result, dict) + assert "device_last_response" in result + + # There must be some redactions in place, so the raw data must not match + assert result["device_last_response"] != diagnostics_data["device_last_response"] + + last_response = result["device_last_response"] + + # We should always have sysinfo available + assert "system" in last_response + assert "get_sysinfo" in last_response["system"] + + sysinfo = last_response["system"]["get_sysinfo"] + for var in sysinfo_vars: + assert sysinfo[var] == "**REDACTED**"