From b596882d8b230d157d46dccc785c09c4629d80fc Mon Sep 17 00:00:00 2001 From: petretiandrea Date: Tue, 5 Mar 2024 00:11:02 +0100 Subject: [PATCH 1/5] added dhcp discovery step and cleanup tracking feature --- custom_components/tapo/__init__.py | 11 +-- custom_components/tapo/config_flow.py | 26 +++--- custom_components/tapo/const.py | 13 +-- custom_components/tapo/discovery.py | 12 ++- custom_components/tapo/entity.py | 4 + custom_components/tapo/helpers.py | 35 ------- custom_components/tapo/light.py | 10 -- custom_components/tapo/manifest.json | 6 ++ custom_components/tapo/migrations.py | 8 +- custom_components/tapo/sensor.py | 10 -- custom_components/tapo/setup_helpers.py | 116 +----------------------- custom_components/tapo/switch.py | 11 --- tests/conftest.py | 2 - tests/test_config_flow.py | 7 +- 14 files changed, 51 insertions(+), 220 deletions(-) diff --git a/custom_components/tapo/__init__.py b/custom_components/tapo/__init__.py index d2cd6d9..b2dec01 100755 --- a/custom_components/tapo/__init__.py +++ b/custom_components/tapo/__init__.py @@ -9,7 +9,7 @@ from custom_components.tapo.discovery import discovery_tapo_devices from custom_components.tapo.errors import DeviceNotSupported from custom_components.tapo.hass_tapo import HassTapo -from custom_components.tapo.migrations import migrate_entry_to_v6 +from custom_components.tapo.migrations import migrate_entry_to_v7 from custom_components.tapo.setup_helpers import create_api_from_config from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry @@ -23,7 +23,6 @@ from .const import CONF_DISCOVERED_DEVICE_INFO from .const import CONF_HOST from .const import CONF_MAC -from .const import CONF_TRACK_DEVICE from .const import DEFAULT_POLLING_RATE_S from .const import DISCOVERY_INTERVAL from .const import DOMAIN @@ -65,10 +64,9 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) - if config_entry.version != 6: - await migrate_entry_to_v6(hass, config_entry) - - _LOGGER.info("Migration to version %s successful", config_entry.version) + if config_entry.version != 7: + await migrate_entry_to_v7(hass, config_entry) + _LOGGER.info("Migration to version %s successful", config_entry.version) return True @@ -113,6 +111,5 @@ def async_create_discovery_flow( CONF_HOST: device.ip, CONF_MAC: mac, CONF_SCAN_INTERVAL: DEFAULT_POLLING_RATE_S, - CONF_TRACK_DEVICE: False, }, ) diff --git a/custom_components/tapo/config_flow.py b/custom_components/tapo/config_flow.py index 0ed98c5..984a0ea 100755 --- a/custom_components/tapo/config_flow.py +++ b/custom_components/tapo/config_flow.py @@ -11,7 +11,6 @@ from custom_components.tapo.const import CONF_HOST from custom_components.tapo.const import CONF_MAC from custom_components.tapo.const import CONF_PASSWORD -from custom_components.tapo.const import CONF_TRACK_DEVICE from custom_components.tapo.const import CONF_USERNAME from custom_components.tapo.const import DEFAULT_POLLING_RATE_S from custom_components.tapo.const import DOMAIN @@ -19,16 +18,19 @@ from custom_components.tapo.const import STEP_DISCOVERY_REQUIRE_AUTH from custom_components.tapo.const import STEP_INIT from custom_components.tapo.const import SUPPORTED_DEVICES +from custom_components.tapo.discovery import discover_tapo_device from custom_components.tapo.errors import CannotConnect from custom_components.tapo.errors import InvalidAuth from custom_components.tapo.errors import InvalidHost from custom_components.tapo.setup_helpers import get_host_port from homeassistant import config_entries from homeassistant import data_entry_flow +from homeassistant.components.dhcp import DhcpServiceInfo from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.typing import DiscoveryInfoType from plugp100.api.tapo_client import TapoClient @@ -50,12 +52,6 @@ CONF_USERNAME, description="The username used with Tapo App, so your email" ): str, vol.Required(CONF_PASSWORD, description="The password used with Tapo App"): str, - vol.Optional( - CONF_TRACK_DEVICE, - description="Try to track device dynamic ip using MAC address. (Your HA must be able to access to same network of device)", - default=False, - ): bool, - vol.Optional(CONF_ADVANCED_SETTINGS, description="Advanced settings"): bool, } ) @@ -92,11 +88,6 @@ def step_options(entry: config_entries.ConfigEntry) -> vol.Schema: description="Polling rate in seconds (e.g. 0.5 seconds means 500ms)", default=entry.data.get(CONF_SCAN_INTERVAL, DEFAULT_POLLING_RATE_S), ): vol.All(vol.Coerce(float), vol.Clamp(min=1)), - vol.Optional( - CONF_TRACK_DEVICE, - description="Try to track device dynamic ip using MAC address. (Your HA must be able to access to same network of device)", - default=entry.data.get(CONF_TRACK_DEVICE, False), - ): bool, } ) @@ -119,6 +110,16 @@ def __init__(self) -> None: self.first_step_data: Optional[FirstStepData] = None self._discovered_info: DiscoveredDevice | None = None + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> data_entry_flow.FlowResult: + """Handle discovery via dhcp.""" + mac_address = dr.format_mac(discovery_info.macaddress) + if discovered_device := await discover_tapo_device(self.hass, mac_address): + return await self._async_handle_discovery( + discovery_info.ip, discovered_device.mac, discovered_device + ) + async def async_step_integration_discovery( self, discovery_info: DiscoveryInfoType ) -> data_entry_flow.FlowResult: @@ -285,7 +286,6 @@ async def _async_create_config_entry_from_device_info( CONF_HOST: info.ip, CONF_MAC: info.mac, CONF_SCAN_INTERVAL: DEFAULT_POLLING_RATE_S, - CONF_TRACK_DEVICE: options.pop(CONF_TRACK_DEVICE, False), }, ) diff --git a/custom_components/tapo/const.py b/custom_components/tapo/const.py index 1ecf0a2..1c90cfd 100755 --- a/custom_components/tapo/const.py +++ b/custom_components/tapo/const.py @@ -55,11 +55,13 @@ SUPPORTED_DEVICE_AS_LED_STRIP = ["l930", "l920", "l900"] -SUPPORTED_DEVICES = SUPPORTED_DEVICE_AS_LED_STRIP\ - + SUPPORTED_DEVICE_AS_LIGHT\ - + SUPPORTED_HUB_DEVICE_MODEL\ - + SUPPORTED_DEVICE_AS_SWITCH\ - + [SUPPORTED_POWER_STRIP_DEVICE_MODEL] +SUPPORTED_DEVICES = ( + SUPPORTED_DEVICE_AS_LED_STRIP + + SUPPORTED_DEVICE_AS_LIGHT + + SUPPORTED_HUB_DEVICE_MODEL + + SUPPORTED_DEVICE_AS_SWITCH + + [SUPPORTED_POWER_STRIP_DEVICE_MODEL] +) ISSUE_URL = "https://github.com/petretiandrea/home-assistant-tapo-p100/issues" @@ -80,7 +82,6 @@ CONF_USERNAME = "username" CONF_PASSWORD = "password" CONF_ADVANCED_SETTINGS = "advanced_settings" -CONF_TRACK_DEVICE = "track_device_mac" CONF_DISCOVERED_DEVICE_INFO = "discovered_device_info" diff --git a/custom_components/tapo/discovery.py b/custom_components/tapo/discovery.py index e636a8c..1b86e98 100644 --- a/custom_components/tapo/discovery.py +++ b/custom_components/tapo/discovery.py @@ -1,5 +1,6 @@ import asyncio from itertools import chain +from typing import Optional from custom_components.tapo.const import DISCOVERY_TIMEOUT from homeassistant.components import network @@ -13,9 +14,7 @@ async def discovery_tapo_devices(hass: HomeAssistant) -> dict[str, DiscoveredDevice]: loop = asyncio.get_event_loop() - broadcast_addresses = await network.async_get_ipv4_broadcast_addresses(hass) - print("Addresses ", broadcast_addresses) discovery_tasks = [ loop.run_in_executor( None, @@ -29,3 +28,12 @@ async def discovery_tapo_devices(hass: HomeAssistant) -> dict[str, DiscoveredDev dr.format_mac(device.mac): device for device in chain(*await asyncio.gather(*discovery_tasks)) } + + +async def discover_tapo_device( + hass: HomeAssistant, mac: str +) -> Optional[DiscoveredDevice]: + found_devices = await discovery_tapo_devices(hass) + return next( + filter(lambda x: dr.format_mac(x.mac) == mac, found_devices.values()), None + ) diff --git a/custom_components/tapo/entity.py b/custom_components/tapo/entity.py index 8f3b126..f9a38f0 100755 --- a/custom_components/tapo/entity.py +++ b/custom_components/tapo/entity.py @@ -4,6 +4,7 @@ from custom_components.tapo.const import DOMAIN from custom_components.tapo.coordinators import TapoDataCoordinator from homeassistant.core import callback +from homeassistant.helpers import device_registry from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from plugp100.responses.device_state import DeviceInfo as TapoDeviceInfo @@ -31,6 +32,9 @@ def device_info(self) -> DeviceInfo: "manufacturer": "TP-Link", "sw_version": self._base_data.firmware_version, "hw_version": self._base_data.hardware_version, + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, self._base_data.mac) + }, } @property diff --git a/custom_components/tapo/helpers.py b/custom_components/tapo/helpers.py index c9f60a5..f889c11 100644 --- a/custom_components/tapo/helpers.py +++ b/custom_components/tapo/helpers.py @@ -1,8 +1,6 @@ -import ipaddress from typing import Optional from typing import TypeVar -from homeassistant.components.network.models import Adapter from homeassistant.util.color import ( color_temperature_kelvin_to_mired as kelvin_to_mired, ) @@ -62,36 +60,3 @@ def tapo_to_hass_color_temperature( max_value=mireds[1], ) return None - - -async def find_adapter_for( - adapters: list[Adapter], ip: Optional[str] -) -> Optional[Adapter]: - default_enabled = next( - iter( - [ - adapter - for adapter in adapters - if adapter.get("enabled") and adapter.get("default") - ] - ), - None, - ) - if ip is None: # search for adapter enabled and default - return default_enabled - else: - for adapter in adapters: - if adapter.get("enabled") and len(adapter.get("ipv4")) > 0: - adapter_network = get_network_of(adapter) - if ipaddress.ip_address(ip) in ipaddress.IPv4Network( - adapter_network, strict=False - ): - return adapter - - return default_enabled - - -def get_network_of(adapter: Adapter) -> Optional[str]: - if len(adapter.get("ipv4")) > 0: - return f"{adapter.get('ipv4')[0].get('address')}/{adapter.get('ipv4')[0].get('network_prefix')}" - return None diff --git a/custom_components/tapo/light.py b/custom_components/tapo/light.py index cae765c..a6d1c44 100755 --- a/custom_components/tapo/light.py +++ b/custom_components/tapo/light.py @@ -34,16 +34,6 @@ _LOGGER = logging.getLogger(__name__) -# async def async_setup_platform( -# hass: HomeAssistant, -# config: Dict[str, Any], -# async_add_entities: AddEntitiesCallback, -# discovery_info=None, -# ) -> None: -# coordinator = await setup_from_platform_config(hass, config) -# _setup_from_coordinator(hass, coordinator, async_add_entities) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): diff --git a/custom_components/tapo/manifest.json b/custom_components/tapo/manifest.json index e1eac07..9f56936 100755 --- a/custom_components/tapo/manifest.json +++ b/custom_components/tapo/manifest.json @@ -7,6 +7,12 @@ "documentation": "https://github.com/petretiandrea/home-assistant-tapo-p100", "issue_tracker": "https://github.com/petretiandrea/home-assistant-tapo-p100/issues", "requirements": ["plugp100==4.0.3"], + "dependencies": ["network"], + "dhcp": [ + { + "registered_devices": true + } + ], "integration_type": "device", "codeowners": ["@petretiandrea"] } diff --git a/custom_components/tapo/migrations.py b/custom_components/tapo/migrations.py index 029696a..defee73 100644 --- a/custom_components/tapo/migrations.py +++ b/custom_components/tapo/migrations.py @@ -1,5 +1,4 @@ from custom_components.tapo.const import CONF_MAC -from custom_components.tapo.const import CONF_TRACK_DEVICE from custom_components.tapo.const import DEFAULT_POLLING_RATE_S from custom_components.tapo.setup_helpers import create_api_from_config from homeassistant.config_entries import ConfigEntry @@ -7,18 +6,17 @@ from homeassistant.core import HomeAssistant -async def migrate_entry_to_v6(hass: HomeAssistant, config_entry: ConfigEntry): - api = create_api_from_config(hass, config_entry) +async def migrate_entry_to_v7(hass: HomeAssistant, config_entry: ConfigEntry): + api = await create_api_from_config(hass, config_entry) new_data = {**config_entry.data} scan_interval = new_data.pop(CONF_SCAN_INTERVAL, DEFAULT_POLLING_RATE_S) mac = (await api.get_device_info()).map(lambda j: j["mac"]).get_or_else(None) - config_entry.version = 6 + config_entry.version = 7 hass.config_entries.async_update_entry( config_entry, data={ **new_data, CONF_MAC: mac, - CONF_TRACK_DEVICE: False, CONF_SCAN_INTERVAL: scan_interval, }, ) diff --git a/custom_components/tapo/sensor.py b/custom_components/tapo/sensor.py index 6bd3cdb..ae0dfde 100644 --- a/custom_components/tapo/sensor.py +++ b/custom_components/tapo/sensor.py @@ -37,16 +37,6 @@ ] -# async def async_setup_platform( -# hass: HomeAssistant, -# config: Dict[str, Any], -# async_add_entities: AddEntitiesCallback, -# discovery_info=None, -# ) -> None: -# coordinator = await setup_from_platform_config(hass, config) -# _setup_from_coordinator(hass, coordinator, async_add_entities) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): diff --git a/custom_components/tapo/setup_helpers.py b/custom_components/tapo/setup_helpers.py index 7760118..85e65ca 100644 --- a/custom_components/tapo/setup_helpers.py +++ b/custom_components/tapo/setup_helpers.py @@ -1,22 +1,13 @@ import logging -from typing import Any -from typing import Dict -from custom_components.tapo.const import CONF_ALTERNATIVE_IP from custom_components.tapo.const import CONF_HOST -from custom_components.tapo.const import CONF_MAC from custom_components.tapo.const import CONF_PASSWORD -from custom_components.tapo.const import CONF_TRACK_DEVICE from custom_components.tapo.const import CONF_USERNAME -from custom_components.tapo.helpers import find_adapter_for -from custom_components.tapo.helpers import get_network_of -from homeassistant.components import network from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession from plugp100.api.tapo_client import TapoClient from plugp100.common.credentials import AuthCredential -from plugp100.discovery.arp_lookup import ArpLookup _LOGGGER = logging.getLogger(__name__) @@ -24,122 +15,19 @@ async def create_api_from_config( hass: HomeAssistant, config: ConfigEntry ) -> TapoClient: - address_finder = TapoAddressFinder(hass, config) credential = AuthCredential( config.data.get(CONF_USERNAME), config.data.get(CONF_PASSWORD) ) - address = await address_finder.lookup() _LOGGGER.debug( "Creating new API to create a coordinator for %s to address %s", config.unique_id, - address, + config.data.get(CONF_HOST), ) session = async_create_clientsession(hass) - host, port = get_host_port(address) + host, port = get_host_port(config.data.get(CONF_HOST)) return TapoClient.create(credential, address=host, port=port, http_session=session) -async def setup_from_platform_config( - hass: HomeAssistant, config: Dict[str, Any] -) -> TapoClient: - temporary_entry = ConfigEntry( - version=1, - domain="", - title="", - source="config_yaml", - data={ - CONF_HOST: config.get(CONF_HOST, config.get(CONF_ALTERNATIVE_IP, None)), - CONF_USERNAME: config.get(CONF_USERNAME), - CONF_PASSWORD: config.get(CONF_PASSWORD), - }, - options={CONF_TRACK_DEVICE: config.get(CONF_TRACK_DEVICE, False)}, - minor_version=1, - ) - return await create_api_from_config(hass, temporary_entry) - - -class TapoAddressFinder: - def __init__(self, hass: HomeAssistant, config: ConfigEntry): - self._hass = hass - self._config = config - - async def lookup(self) -> str: - if ( - self._config.data.get(CONF_TRACK_DEVICE, False) - and self._config.data.get(CONF_MAC, None) is not None - ): - if self._config.data.get(CONF_MAC, None) is not None: - return await try_track_ip_address( - self._hass, - self._config.data.get(CONF_MAC), - self._config.data.get(CONF_HOST), - ) - else: - logging.warning( - "Tracking mac address enabled, but no MAC address found on config entry" - ) - return self._config.data.get(CONF_HOST) - else: - return self._config.data.get(CONF_HOST) - - -async def try_track_ip_address( - hass: HomeAssistant, mac: str, last_known_ip: str -) -> str: - _LOGGGER.info( - "Trying to track ip address of %s, last known ip is %s", mac, last_known_ip - ) - adapters = await network.async_get_adapters(hass) - adapter = await find_adapter_for(adapters, last_known_ip) - try: - if adapter is not None: - target_network = get_network_of(adapter) - device_ip = await ArpLookup.lookup( - mac.replace("-", ":"), target_network, timeout=5 - ) - return device_ip.get_or_else(last_known_ip) - else: - _LOGGGER.warning( - "No adapter found for %s with last ip %s", mac, last_known_ip - ) - except PermissionError: - _LOGGGER.warning("No permission to scan network") - - return last_known_ip - - -# async def setup_from_platform_config( -# hass: HomeAssistant, config: Dict[str, Any] -# ) -> TapoCoordinator: -# temporary_entry = ConfigEntry( -# version=1, -# domain="", -# title="", -# source="config_yaml", -# data={ -# CONF_HOST: config.get(CONF_HOST, config.get(CONF_ALTERNATIVE_IP, None)), -# CONF_USERNAME: config.get(CONF_USERNAME), -# CONF_PASSWORD: config.get(CONF_PASSWORD), -# }, -# options={CONF_TRACK_DEVICE: config.get(CONF_TRACK_DEVICE, False)}, -# ) -# client = await create_api_from_config(hass, temporary_entry) -# state = ( -# (await client.get_device_info()) -# .map(lambda x: TapoDeviceInfo(**x)) -# .get_or_raise() -# ) -# return await create_coordinator( -# hass, -# client, -# temporary_entry.data.get(CONF_HOST), -# polling_interval=timedelta( -# seconds=config.get(CONF_SCAN_INTERVAL, DEFAULT_POLLING_RATE_S) -# ), -# device_info=state, -# ) - - def get_host_port(host_user_input: str) -> (str, int): if ":" in host_user_input: parts = host_user_input.split(":", 1) diff --git a/custom_components/tapo/switch.py b/custom_components/tapo/switch.py index fe0a0f8..782df87 100755 --- a/custom_components/tapo/switch.py +++ b/custom_components/tapo/switch.py @@ -23,17 +23,6 @@ from plugp100.responses.device_state import PlugDeviceState -# async def async_setup_platform( -# hass: HomeAssistant, -# config: Dict[str, Any], -# async_add_entities: AddEntitiesCallback, -# discovery_info=None, -# ) -> None: -# coordinator = await setup_from_platform_config(hass, config) -# if isinstance(coordinator, SingleDeviceCoordinator): -# async_add_entities([TapoPlugEntity(coordinator)], True) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ): diff --git a/tests/conftest.py b/tests/conftest.py index 3c061f2..cc3f61b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,7 +7,6 @@ import pytest from custom_components.tapo.const import CONF_HOST from custom_components.tapo.const import CONF_PASSWORD -from custom_components.tapo.const import CONF_TRACK_DEVICE from custom_components.tapo.const import CONF_USERNAME from custom_components.tapo.const import DOMAIN from custom_components.tapo.const import TapoDevice @@ -95,7 +94,6 @@ async def setup_platform( CONF_USERNAME: "mock", CONF_PASSWORD: "mock", CONF_SCAN_INTERVAL: 5000, - CONF_TRACK_DEVICE: False, }, version=6, unique_id=state.value.info.device_id, diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 31732f0..59c1456 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,9 +1,9 @@ from custom_components.tapo import CONF_DISCOVERED_DEVICE_INFO from custom_components.tapo import CONF_HOST from custom_components.tapo import CONF_MAC -from custom_components.tapo import CONF_TRACK_DEVICE from custom_components.tapo import DEFAULT_POLLING_RATE_S from custom_components.tapo import DOMAIN +from custom_components.tapo.const import STEP_DISCOVERY_REQUIRE_AUTH from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD from homeassistant.const import CONF_SCAN_INTERVAL @@ -12,7 +12,6 @@ from homeassistant.data_entry_flow import FlowResultType from plugp100.discovery.discovered_device import DiscoveredDevice -from custom_components.tapo.const import STEP_DISCOVERY_REQUIRE_AUTH from .conftest import IP_ADDRESS from .conftest import MAC_ADDRESS @@ -31,7 +30,6 @@ async def test_discovery_auth( CONF_HOST: IP_ADDRESS, CONF_MAC: MAC_ADDRESS, CONF_SCAN_INTERVAL: DEFAULT_POLLING_RATE_S, - CONF_TRACK_DEVICE: False, }, ) await hass.async_block_till_done() @@ -51,6 +49,5 @@ async def test_discovery_auth( assert auth_result["data"][CONF_USERNAME] == "fake_username" assert auth_result["data"][CONF_PASSWORD] == "fake_password" assert auth_result["data"][CONF_HOST] == mock_discovery.ip - assert auth_result["data"][CONF_TRACK_DEVICE] is False assert auth_result["data"][CONF_SCAN_INTERVAL] == 30 - assert auth_result["context"][CONF_DISCOVERED_DEVICE_INFO] == mock_discovery \ No newline at end of file + assert auth_result["context"][CONF_DISCOVERED_DEVICE_INFO] == mock_discovery From d02cb55b5201fc6ab96b422f144349b6d238251f Mon Sep 17 00:00:00 2001 From: petretiandrea Date: Tue, 5 Mar 2024 00:18:39 +0100 Subject: [PATCH 2/5] fix tests --- tests/conftest.py | 2 +- tests/fixtures/bulb.json | 2 +- tests/fixtures/discovery.json | 2 +- tests/test_config_flow.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index cc3f61b..f5773b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,7 +95,7 @@ async def setup_platform( CONF_PASSWORD: "mock", CONF_SCAN_INTERVAL: 5000, }, - version=6, + version=7, unique_id=state.value.info.device_id, ) config_entry.add_to_hass(hass) diff --git a/tests/fixtures/bulb.json b/tests/fixtures/bulb.json index 48c17ff..3ae9384 100644 --- a/tests/fixtures/bulb.json +++ b/tests/fixtures/bulb.json @@ -104,7 +104,7 @@ ] }, "get_device_info": { - "device_id": "8023971BED756926E6ACA5660A9ADAFB1FC069C8", + "device_id": "device_id_123", "fw_ver": "1.1.0 Build 230721 Rel.224802", "hw_ver": "2.0", "type": "SMART.TAPOBULB", diff --git a/tests/fixtures/discovery.json b/tests/fixtures/discovery.json index db4ba1e..1a608e3 100644 --- a/tests/fixtures/discovery.json +++ b/tests/fixtures/discovery.json @@ -1,5 +1,5 @@ { - "device_id": "device_id", + "device_id": "device_id_123", "owner": "owner_id", "device_type": "SMART.TAPOBULB", "device_model": "L530E(EU)", diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 59c1456..25cc969 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -45,7 +45,7 @@ async def test_discovery_auth( ) assert auth_result["type"] is FlowResultType.CREATE_ENTRY - assert auth_result["context"]["unique_id"] == MAC_ADDRESS + assert auth_result["context"]["unique_id"] == mock_discovery.device_id assert auth_result["data"][CONF_USERNAME] == "fake_username" assert auth_result["data"][CONF_PASSWORD] == "fake_password" assert auth_result["data"][CONF_HOST] == mock_discovery.ip From 2fc0d38db11341d186f0c7da9f3a1e411b329b0a Mon Sep 17 00:00:00 2001 From: petretiandrea Date: Tue, 5 Mar 2024 00:38:22 +0100 Subject: [PATCH 3/5] fix tests --- requirements_dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements_dev.txt b/requirements_dev.txt index 4145abd..664d396 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,3 +4,4 @@ pre-commit==3.3.3 reorder-python-imports==3.10.0 flake8==6.1.0 autoflake==2.2.1 +aiodiscover==1.6.0 \ No newline at end of file From f9a38264525e2470631588211da6176a5eda8608 Mon Sep 17 00:00:00 2001 From: petretiandrea Date: Tue, 5 Mar 2024 08:23:54 +0100 Subject: [PATCH 4/5] added dhcp change ip test --- custom_components/tapo/hub/__init__.py | 0 tests/conftest.py | 2 +- tests/test_config_flow.py | 41 ++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 custom_components/tapo/hub/__init__.py diff --git a/custom_components/tapo/hub/__init__.py b/custom_components/tapo/hub/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index f5773b1..62f9854 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,7 +56,7 @@ def expected_lingering_tasks() -> bool: @pytest.fixture() -def mock_discovery() -> DiscoveredDevice: +def mock_discovery(): (discovered_device, device_info) = mock_discovered_device() with patch( "custom_components.tapo.discovery_tapo_devices", diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 25cc969..6595e70 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,3 +1,8 @@ +from unittest.mock import patch, AsyncMock + +from homeassistant.components import dhcp +from pytest_homeassistant_custom_component.common import MockConfigEntry + from custom_components.tapo import CONF_DISCOVERED_DEVICE_INFO from custom_components.tapo import CONF_HOST from custom_components.tapo import CONF_MAC @@ -51,3 +56,39 @@ async def test_discovery_auth( assert auth_result["data"][CONF_HOST] == mock_discovery.ip assert auth_result["data"][CONF_SCAN_INTERVAL] == 30 assert auth_result["context"][CONF_DISCOVERED_DEVICE_INFO] == mock_discovery + +async def test_discovery_ip_change_dhcp( + hass: HomeAssistant, mock_discovery: DiscoveredDevice +) -> None: + mock_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: mock_discovery.ip, + CONF_USERNAME: "mock", + CONF_PASSWORD: "mock", + CONF_SCAN_INTERVAL: 5000, + }, + version=7, + unique_id=mock_discovery.device_id, + ) + with patch("plugp100.api.tapo_client.TapoClient.get_device_info", AsyncMock(side_effect=Exception("Something wrong"))): + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state == config_entries.ConfigEntryState.SETUP_RETRY + + with patch( + "custom_components.tapo.config_flow.discover_tapo_device", + AsyncMock(return_value=mock_discovery), + ): + discovery_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="127.0.0.2", macaddress=MAC_ADDRESS, hostname="hostname" + ), + ) + assert discovery_result["type"] is FlowResultType.ABORT + assert discovery_result["reason"] == "already_configured" + assert mock_config_entry.data[CONF_HOST] == "127.0.0.2" From dbf2c6d372d0417f59775865202b049c5671a5d0 Mon Sep 17 00:00:00 2001 From: petretiandrea Date: Tue, 5 Mar 2024 08:28:02 +0100 Subject: [PATCH 5/5] update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1cc1b09..798caf9 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,12 @@ tapo: This will enable tapo device discovery. Not all tapo devices supports tapo discovery, so if you not find it, try adding manually. Also tapo integration discovery filters out not supported devices! +#### Device IP Tracking +By using DHCP home assistant discovery the feature of mac tracking is now disabled, cause HA can track it automatically now, please be sure +to have DHCP discovery not disable on your `configuration.yaml` (by default is active). + +[BREAKING] Tracking mac address feature is now disabled cause not recommended by HA. The tracking is now performed by HA itself. + ### Supported devices - [x] pure async home assistant's method @@ -65,8 +71,6 @@ Also tapo integration discovery filters out not supported devices! ### Additional features -- [x] tracking of ip address. Cause Tapo local discovery isn't working for a lot of Tapo devices, this integration can try to track ip address changes by reling on MAC address. - This requires Home Assistant to runs on the same devices network and the capability to send ARP and broadcast packets. - [x] manually change ip address. Now you can change the ip address of a tapo device wihtout removing and re-adding it. # How to install