Skip to content

Commit

Permalink
fix(core): mac address as unique id config flow (#713)
Browse files Browse the repository at this point in the history
  • Loading branch information
petretiandrea authored Mar 5, 2024
1 parent f6b7baf commit bd50c7c
Show file tree
Hide file tree
Showing 11 changed files with 66 additions and 43 deletions.
3 changes: 3 additions & 0 deletions .releaserc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
{
"type": "breaking",
"release": "major"
},
{ "scope": "ci",
"release": false
}
]
}
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,18 @@ For some unknown reason email with capital letter thrown an "Invalid authenticat
## Features

### Discovery

The integration now supports native tapo discovery! To enable it you must add at least one tapo device or this line to your tapo configuration file

```
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).

Expand Down Expand Up @@ -113,12 +117,12 @@ This video show installation steps:
<!---->

## Configuration by configuration.yaml
[BREAKING]

[BREAKING]

The latest version of this integration remove configuration.yaml device configuration support. This
is due to follow home assistant best practices https://developers.home-assistant.io/docs/configuration_yaml_index/ and https://github.com/home-assistant/architecture/blob/master/adr/0010-integration-configuration.md#decision


## Contributions are welcome!

Open a pull request, every contribution are welcome.
Expand Down
9 changes: 5 additions & 4 deletions custom_components/tapo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
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_v7
from custom_components.tapo.migrations import migrate_entry_to_v8
from custom_components.tapo.setup_helpers import create_api_from_config
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import discovery_flow
from homeassistant.helpers.event import async_track_time_interval
from plugp100.discovery.discovered_device import DiscoveredDevice
Expand Down Expand Up @@ -64,8 +65,8 @@ 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 != 7:
await migrate_entry_to_v7(hass, config_entry)
if config_entry.version != 8:
await migrate_entry_to_v8(hass, config_entry)
_LOGGER.info("Migration to version %s successful", config_entry.version)

return True
Expand Down Expand Up @@ -109,7 +110,7 @@ def async_create_discovery_flow(
},
data={
CONF_HOST: device.ip,
CONF_MAC: mac,
CONF_MAC: dr.format_mac(mac),
CONF_SCAN_INTERVAL: DEFAULT_POLLING_RATE_S,
},
)
13 changes: 6 additions & 7 deletions custom_components/tapo/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class FirstStepData:
class TapoConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for tapo."""

VERSION = 4
VERSION = 5
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

def __init__(self) -> None:
Expand All @@ -117,7 +117,7 @@ async def async_step_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
discovery_info.ip, mac_address, discovered_device
)

async def async_step_integration_discovery(
Expand All @@ -141,7 +141,7 @@ async def async_step_user(
if user_input is not None:
try:
device_info = await self._async_get_device_info(user_input)
await self.async_set_unique_id(device_info.device_id)
await self.async_set_unique_id(dr.format_mac(device_info.mac))
self._abort_if_unique_id_configured()
self._async_abort_entries_match({CONF_HOST: device_info.ip})

Expand Down Expand Up @@ -196,7 +196,6 @@ async def async_step_advanced_config(
errors=errors,
)

# TODO: use mac address as unique id
async def _async_handle_discovery(
self,
host: str,
Expand All @@ -205,7 +204,7 @@ async def _async_handle_discovery(
) -> data_entry_flow.FlowResult:
self._discovered_info = discovered_device
existing_entry = await self.async_set_unique_id(
discovered_device.device_id, raise_on_progress=False
mac_address, raise_on_progress=False
)
if existing_entry:
if result := self._recover_config_on_entry_error(
Expand All @@ -232,7 +231,7 @@ async def async_step_discovery_auth_confirm(
device_info = await self._async_get_device_info_from_discovered(
self._discovered_info, user_input
)
await self.async_set_unique_id(device_info.device_id)
await self.async_set_unique_id(dr.format_mac(device_info.mac))
self._abort_if_unique_id_configured()
except InvalidAuth as error:
errors["base"] = "invalid_auth"
Expand Down Expand Up @@ -337,7 +336,7 @@ async def async_step_init(
)
return self.async_create_entry(title="", data={})
return self.async_show_form(
step_id=STEP_INIT,
step_id="init",
data_schema=step_options(self.config_entry),
)

Expand Down
25 changes: 14 additions & 11 deletions custom_components/tapo/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,22 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr


async def migrate_entry_to_v7(hass: HomeAssistant, config_entry: ConfigEntry):
async def migrate_entry_to_v8(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 = 7
hass.config_entries.async_update_entry(
config_entry,
data={
**new_data,
CONF_MAC: mac,
CONF_SCAN_INTERVAL: scan_interval,
},
)
if mac := (await api.get_device_info()).map(lambda j: j["mac"]).get_or_else(None):
config_entry.version = 8
hass.config_entries.async_update_entry(
config_entry,
data={
**new_data,
CONF_MAC: dr.format_mac(mac),
CONF_SCAN_INTERVAL: scan_interval,
},
)
else:
raise Exception("Failed to fetch data to migrate entity")
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +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
aiodiscover==1.6.0
12 changes: 10 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from custom_components.tapo.const import TapoDevice
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from plugp100.api.hub.hub_device import HubDevice
from plugp100.api.ledstrip_device import LedStripDevice
Expand Down Expand Up @@ -95,8 +96,8 @@ async def setup_platform(
CONF_PASSWORD: "mock",
CONF_SCAN_INTERVAL: 5000,
},
version=7,
unique_id=state.value.info.device_id,
version=8,
unique_id=dr.format_mac(state.value.info.mac),
)
config_entry.add_to_hass(hass)
with patch(
Expand Down Expand Up @@ -157,6 +158,7 @@ def mock_plug(with_emeter: bool = False) -> MagicMock:
device.get_current_power = AsyncMock(return_value=power)
device.get_energy_usage = AsyncMock(return_value=energy)

device.device_id = state.value.info.device_id
return device


Expand Down Expand Up @@ -184,6 +186,7 @@ def mock_hub(with_children: bool = False) -> MagicMock:
)
device.get_children = AsyncMock(return_value=children)
device.__class__ = HubDevice
device.device_id = state.value.info.device_id
return device


Expand All @@ -206,6 +209,8 @@ def mock_bulb(components_to_exclude: list[str] = []) -> MagicMock:
device.get_state = AsyncMock(return_value=state)
device.get_component_negotiation = AsyncMock(return_value=components)
device.__class__ = LightDevice

device.device_id = state.value.info.device_id
return device


Expand All @@ -228,6 +233,7 @@ def mock_led_strip() -> MagicMock:
device.get_state = AsyncMock(return_value=state)
device.get_component_negotiation = AsyncMock(return_value=components)
device.__class__ = LedStripDevice
device.device_id = state.value.info.device_id
return device


Expand Down Expand Up @@ -274,4 +280,6 @@ def mock_plug_strip() -> MagicMock:
)
device.get_children = AsyncMock(return_value=children)
device.__class__ = PowerStripDevice
device.device_id = state.value.info.device_id

return device
4 changes: 2 additions & 2 deletions tests/test_binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
)
async def test_switch_overheat(hass: HomeAssistant, device: MagicMock):
device_registry = dr.async_get(hass)
entry = await setup_platform(hass, device, [BINARY_SENSOR_DOMAIN])
await setup_platform(hass, device, [BINARY_SENSOR_DOMAIN])
entity_id = await extract_entity_id(device, BINARY_SENSOR_DOMAIN, "overheat")
assert len(hass.states.async_all()) == 1

state_entity = hass.states.get(entity_id)
device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)})
device = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)})
assert state_entity is not None
assert state_entity.state == "off"
assert state_entity.attributes["device_class"] == "heat"
Expand Down
25 changes: 15 additions & 10 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from unittest.mock import patch, AsyncMock

from homeassistant.components import dhcp
from pytest_homeassistant_custom_component.common import MockConfigEntry
from unittest.mock import AsyncMock
from unittest.mock import patch

from custom_components.tapo import CONF_DISCOVERED_DEVICE_INFO
from custom_components.tapo import CONF_HOST
Expand All @@ -10,12 +8,15 @@
from custom_components.tapo import DOMAIN
from custom_components.tapo.const import STEP_DISCOVERY_REQUIRE_AUTH
from homeassistant import config_entries
from homeassistant.components import dhcp
from homeassistant.const import CONF_PASSWORD
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.const import CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr
from plugp100.discovery.discovered_device import DiscoveredDevice
from pytest_homeassistant_custom_component.common import MockConfigEntry

from .conftest import IP_ADDRESS
from .conftest import MAC_ADDRESS
Expand Down Expand Up @@ -50,13 +51,14 @@ async def test_discovery_auth(
)

assert auth_result["type"] is FlowResultType.CREATE_ENTRY
assert auth_result["context"]["unique_id"] == mock_discovery.device_id
assert auth_result["context"]["unique_id"] == MAC_ADDRESS
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_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:
Expand All @@ -68,19 +70,22 @@ async def test_discovery_ip_change_dhcp(
CONF_PASSWORD: "mock",
CONF_SCAN_INTERVAL: 5000,
},
version=7,
unique_id=mock_discovery.device_id,
version=8,
unique_id=dr.format_mac(mock_discovery.mac),
)
with patch("plugp100.api.tapo_client.TapoClient.get_device_info", AsyncMock(side_effect=Exception("Something wrong"))):
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),
"custom_components.tapo.config_flow.discover_tapo_device",
AsyncMock(return_value=mock_discovery),
):
discovery_result = await hass.config_entries.flow.async_init(
DOMAIN,
Expand Down
4 changes: 2 additions & 2 deletions tests/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
)
async def test_signal_sensor(hass: HomeAssistant, device: MagicMock):
device_registry = dr.async_get(hass)
entry = await setup_platform(hass, device, [SENSOR_DOMAIN])
await setup_platform(hass, device, [SENSOR_DOMAIN])
state_entity = hass.states.get(
await extract_entity_id(device, SENSOR_DOMAIN, "signal_level")
)
device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)})
device = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)})
assert state_entity is not None
assert state_entity.state is not None
assert state_entity.attributes["device_class"] == "signal_strength"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
async def test_switch_setup(hass: HomeAssistant):
device_registry = dr.async_get(hass)
device = mock_plug()
entry = await setup_platform(hass, device, [SWITCH_DOMAIN])
await setup_platform(hass, device, [SWITCH_DOMAIN])
entity_id = await extract_entity_id(device, SWITCH_DOMAIN)
state_entity = hass.states.get(entity_id)
device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)})
device = device_registry.async_get_device(identifiers={(DOMAIN, device.device_id)})
assert state_entity is not None
assert state_entity.state == "on"
assert state_entity.attributes["device_class"] == "outlet"
Expand Down

0 comments on commit bd50c7c

Please sign in to comment.