1 """Component to embed TP-Link smart home devices."""
3 from __future__
import annotations
6 from collections.abc
import Iterable
7 from datetime
import timedelta
11 from aiohttp
import ClientSession
20 from kasa.httpclient
import get_cookie_jar
21 from kasa.iot
import IotStrip
23 from homeassistant
import config_entries
40 config_validation
as cv,
41 device_registry
as dr,
50 CONF_CONFIG_ENTRY_MINOR_VERSION,
51 CONF_CONNECTION_PARAMETERS,
52 CONF_CREDENTIALS_HASH,
60 from .coordinator
import TPLinkDataUpdateCoordinator
61 from .models
import TPLinkData
63 type TPLinkConfigEntry = ConfigEntry[TPLinkData]
66 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
68 _LOGGER = logging.getLogger(__name__)
72 """Return aiohttp clientsession with cookie jar configured."""
74 hass, verify_ssl=
False, cookie_jar=get_cookie_jar()
81 discovered_devices: dict[str, Device],
83 """Trigger config flows for discovered devices."""
85 for formatted_mac, device
in discovered_devices.items():
86 discovery_flow.async_create_flow(
89 context={
"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
91 CONF_ALIAS: device.alias
or mac_alias(device.mac),
92 CONF_HOST: device.host,
93 CONF_MAC: formatted_mac,
100 """Discover TPLink devices on configured network interfaces."""
103 broadcast_addresses = await network.async_get_ipv4_broadcast_addresses(hass)
107 discovery_timeout=DISCOVERY_TIMEOUT,
108 timeout=CONNECT_TIMEOUT,
109 credentials=credentials,
111 for address
in broadcast_addresses
113 discovered_devices: dict[str, Device] = {}
114 for device_list
in await asyncio.gather(*tasks):
115 for device
in device_list.values():
116 discovered_devices[dr.format_mac(device.mac)] = device
117 return discovered_devices
120 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
121 """Set up the TP-Link component."""
122 hass.data.setdefault(DOMAIN, {})
124 async
def _async_discovery(*_: Any) ->
None:
128 hass.async_create_background_task(
129 _async_discovery(),
"tplink first discovery", eager_start=
True
132 hass, _async_discovery, DISCOVERY_INTERVAL, cancel_on_shutdown=
True
139 """Set up TPLink from a config entry."""
140 host: str = entry.data[CONF_HOST]
142 entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH)
143 entry_use_http = entry.data.get(CONF_USES_HTTP,
False)
144 entry_aes_keys = entry.data.get(CONF_AES_KEYS)
145 port_override = entry.data.get(CONF_PORT)
147 conn_params: Device.ConnectionParameters |
None =
None
148 if conn_params_dict := entry.data.get(CONF_CONNECTION_PARAMETERS):
150 conn_params = Device.ConnectionParameters.from_dict(conn_params_dict)
151 except (KasaException, TypeError, ValueError, LookupError):
153 "Invalid connection parameters dict for %s: %s", host, conn_params_dict
157 config = DeviceConfig(
159 timeout=CONNECT_TIMEOUT,
161 aes_keys=entry_aes_keys,
162 port_override=port_override,
165 config.connection_type = conn_params
168 config.credentials = credentials
169 elif entry_credentials_hash:
170 config.credentials_hash = entry_credentials_hash
173 device: Device = await Device.connect(config=config)
174 except AuthenticationError
as ex:
176 if not credentials
and entry_credentials_hash:
177 data = {k: v
for k, v
in entry.data.items()
if k != CONF_CREDENTIALS_HASH}
178 hass.config_entries.async_update_entry(entry, data=data)
179 raise ConfigEntryAuthFailed
from ex
180 except KasaException
as ex:
181 raise ConfigEntryNotReady
from ex
183 device_credentials_hash = device.credentials_hash
188 updates: dict[str, Any] = {}
189 if device_credentials_hash
and device_credentials_hash != entry_credentials_hash:
190 updates[CONF_CREDENTIALS_HASH] = device_credentials_hash
191 if entry_aes_keys != device.config.aes_keys:
192 updates[CONF_AES_KEYS] = device.config.aes_keys
193 if entry.data.get(CONF_ALIAS) != device.alias:
194 updates[CONF_ALIAS] = device.alias
195 if entry.data.get(CONF_MODEL) != device.model:
196 updates[CONF_MODEL] = device.model
198 hass.config_entries.async_update_entry(
205 found_mac = dr.format_mac(device.mac)
206 if found_mac != entry.unique_id:
213 f
"Unexpected device found at {host}; expected {entry.unique_id}, found {found_mac}"
217 child_coordinators: list[TPLinkDataUpdateCoordinator] = []
221 if isinstance(device, IotStrip):
222 child_coordinators = [
226 for child
in device.children
229 entry.runtime_data =
TPLinkData(parent_coordinator, child_coordinators)
230 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
236 """Unload a config entry."""
237 data = entry.runtime_data
238 device = data.parent_coordinator.device
239 unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
240 await device.protocol.close()
246 """Convert the device id so it matches what was used in the original version."""
247 device_id: str = device.device_id
250 if "_" not in device_id:
252 return device_id.split(
"_")[1]
256 """Get a name for the device. alias can be none on some devices."""
265 for child
in parent.children
266 if child.device_type
is device.device_type
268 suffix = f
" {devices.index(device.device_id) + 1}" if len(devices) > 1
else ""
269 return f
"{device.device_type.value.capitalize()}{suffix}"
270 return f
"Unnamed {device.model}"
274 """Retrieve the credentials from hass data."""
275 if DOMAIN
in hass.data
and CONF_AUTHENTICATION
in hass.data[DOMAIN]:
276 auth = hass.data[DOMAIN][CONF_AUTHENTICATION]
277 return Credentials(auth[CONF_USERNAME], auth[CONF_PASSWORD])
283 """Save the credentials to HASS data."""
284 hass.data.setdefault(DOMAIN, {})[CONF_AUTHENTICATION] = {
285 CONF_USERNAME: username,
286 CONF_PASSWORD: password,
291 """Convert a MAC address to a short address for the UI."""
292 return mac.replace(
":",
"")[-4:].upper()
299 for type_, conn
in device.connections
300 if type_ == dr.CONNECTION_NETWORK_MAC
310 upper_mac = mac.upper()
312 (device_id
for device_id
in device_ids
if device_id.upper() == upper_mac),
318 """Migrate old entry."""
319 entry_version = config_entry.version
320 entry_minor_version = config_entry.minor_version
323 config_flow_minor_version = CONF_CONFIG_ENTRY_MINOR_VERSION
325 new_minor_version = 3
328 and entry_minor_version < new_minor_version <= config_flow_minor_version
331 "Migrating from version %s.%s", entry_version, entry_minor_version
339 dev_reg = dr.async_get(hass)
340 for device
in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id):
341 original_identifiers = device.identifiers
343 tplink_identifiers = [
344 ident[1]
for ident
in original_identifiers
if ident[0] == DOMAIN
348 if len(tplink_identifiers) <= 1
or not (
354 mac, tplink_identifiers
359 "Unable to replace identifiers for device %s (%s): %s",
367 ident
for ident
in device.identifiers
if ident[0] != DOMAIN
369 new_identifiers.add((DOMAIN, tplink_parent_device_id))
370 dev_reg.async_update_device(device.id, new_identifiers=new_identifiers)
372 "Replaced identifiers for device %s (%s): %s with: %s",
375 original_identifiers,
379 hass.config_entries.async_update_entry(
380 config_entry, minor_version=new_minor_version
384 "Migration to version %s.%s complete", entry_version, new_minor_version
387 new_minor_version = 4
390 and entry_minor_version < new_minor_version <= config_flow_minor_version
393 updates: dict[str, Any] = {}
394 if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
395 assert isinstance(config_dict, dict)
396 if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH,
None):
397 updates[CONF_CREDENTIALS_HASH] = credentials_hash
398 updates[CONF_DEVICE_CONFIG] = config_dict
399 hass.config_entries.async_update_entry(
405 minor_version=new_minor_version,
408 "Migration to version %s.%s complete", entry_version, new_minor_version
411 new_minor_version = 5
414 and entry_minor_version < new_minor_version <= config_flow_minor_version
420 k: v
for k, v
in config_entry.data.items()
if k != CONF_DEVICE_CONFIG
422 if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
423 assert isinstance(config_dict, dict)
424 if connection_parameters := config_dict.get(
"connection_type"):
425 updates[CONF_CONNECTION_PARAMETERS] = connection_parameters
426 if (use_http := config_dict.get(CONF_USES_HTTP))
is not None:
427 updates[CONF_USES_HTTP] = use_http
428 hass.config_entries.async_update_entry(
434 minor_version=new_minor_version,
437 "Migration to version %s.%s complete", entry_version, new_minor_version
aiohttp.ClientSession async_create_clientsession()
dict[str, Device] async_discover_devices(HomeAssistant hass)
bool async_setup_entry(HomeAssistant hass, TPLinkConfigEntry entry)
Credentials|None get_credentials(HomeAssistant hass)
None set_credentials(HomeAssistant hass, str username, str password)
bool async_unload_entry(HomeAssistant hass, TPLinkConfigEntry entry)
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
str get_device_name(Device device, Device|None parent=None)
ClientSession create_async_tplink_clientsession(HomeAssistant hass)
str|None _mac_connection_or_none(dr.DeviceEntry device)
None async_trigger_discovery(HomeAssistant hass, dict[str, Device] discovered_devices)
bool async_setup(HomeAssistant hass, ConfigType config)
str|None _device_id_is_mac_or_none(str mac, Iterable[str] device_ids)
str legacy_device_id(Device device)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)