diff --git a/homeassistant/components/tplink_lte/__init__.py b/homeassistant/components/tplink_lte/__init__.py index ca9b8311ebe..9713a1aa227 100644 --- a/homeassistant/components/tplink_lte/__init__.py +++ b/homeassistant/components/tplink_lte/__init__.py @@ -1,172 +1,30 @@ -"""Support for TP-Link LTE modems.""" +"""The tplink_lte integration.""" -from __future__ import annotations - -import asyncio -import logging -from typing import Any - -import aiohttp -import attr -import tp_connected import voluptuous as vol -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_RECIPIENT, - EVENT_HOMEASSISTANT_STOP, - Platform, -) -from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, issue_registry as ir from homeassistant.helpers.typing import ConfigType -_LOGGER = logging.getLogger(__name__) - DOMAIN = "tplink_lte" -DATA_KEY = "tplink_lte" - -CONF_NOTIFY = "notify" - -_NOTIFY_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_RECIPIENT): vol.All(cv.ensure_list, [cv.string]), - } -) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NOTIFY): vol.All( - cv.ensure_list, [_NOTIFY_SCHEMA] - ), - } - ) - ], - ) - }, + {DOMAIN: cv.match_all}, extra=vol.ALLOW_EXTRA, ) -@attr.s -class ModemData: - """Class for modem state.""" - - host: str = attr.ib() - modem: tp_connected.Modem = attr.ib() - - connected: bool = attr.ib(init=False, default=True) - - -@attr.s -class LTEData: - """Shared state.""" - - websession: aiohttp.ClientSession = attr.ib() - modem_data: dict[str, ModemData] = attr.ib(init=False, factory=dict) - - def get_modem_data(self, config: dict[str, Any]) -> ModemData | None: - """Get the requested or the only modem_data value.""" - if CONF_HOST in config: - return self.modem_data.get(config[CONF_HOST]) - if len(self.modem_data) == 1: - return next(iter(self.modem_data.values())) - - return None - - async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up TP-Link LTE component.""" - if DATA_KEY not in hass.data: - websession = async_create_clientsession( - hass, cookie_jar=aiohttp.CookieJar(unsafe=True) - ) - hass.data[DATA_KEY] = LTEData(websession) - - domain_config = config.get(DOMAIN, []) - - tasks = [_setup_lte(hass, conf) for conf in domain_config] - if tasks: - await asyncio.gather(*tasks) - - for conf in domain_config: - for notify_conf in conf.get(CONF_NOTIFY, []): - hass.async_create_task( - discovery.async_load_platform( - hass, Platform.NOTIFY, DOMAIN, notify_conf, config - ) - ) - + ir.async_create_issue( + hass, + DOMAIN, + DOMAIN, + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="integration_removed", + translation_placeholders={ + "ghsa_url": "https://github.com/advisories/GHSA-h95x-26f3-88hr", + }, + ) return True - - -async def _setup_lte( - hass: HomeAssistant, lte_config: dict[str, Any], delay: int = 0 -) -> None: - """Set up a TP-Link LTE modem.""" - - host: str = lte_config[CONF_HOST] - password: str = lte_config[CONF_PASSWORD] - - lte_data: LTEData = hass.data[DATA_KEY] - modem = tp_connected.Modem(hostname=host, websession=lte_data.websession) - - modem_data = ModemData(host, modem) - - try: - await _login(hass, modem_data, password) - except tp_connected.Error: - retry_task = hass.loop.create_task(_retry_login(hass, modem_data, password)) - - @callback - def cleanup_retry(event: Event) -> None: - """Clean up retry task resources.""" - if not retry_task.done(): - retry_task.cancel() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_retry) - - -async def _login(hass: HomeAssistant, modem_data: ModemData, password: str) -> None: - """Log in and complete setup.""" - await modem_data.modem.login(password=password) - modem_data.connected = True - lte_data: LTEData = hass.data[DATA_KEY] - lte_data.modem_data[modem_data.host] = modem_data - - async def cleanup(event: Event) -> None: - """Clean up resources.""" - await modem_data.modem.logout() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) - - -async def _retry_login( - hass: HomeAssistant, modem_data: ModemData, password: str -) -> None: - """Sleep and retry setup.""" - - _LOGGER.warning("Could not connect to %s. Will keep trying", modem_data.host) - - modem_data.connected = False - delay = 15 - - while not modem_data.connected: - await asyncio.sleep(delay) - - try: - await _login(hass, modem_data, password) - _LOGGER.warning("Connected to %s", modem_data.host) - except tp_connected.Error: - delay = min(2 * delay, 300) diff --git a/homeassistant/components/tplink_lte/manifest.json b/homeassistant/components/tplink_lte/manifest.json index a880594e683..1f9057c3ad7 100644 --- a/homeassistant/components/tplink_lte/manifest.json +++ b/homeassistant/components/tplink_lte/manifest.json @@ -4,7 +4,6 @@ "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/tplink_lte", "iot_class": "local_polling", - "loggers": ["tp_connected"], "quality_scale": "legacy", - "requirements": ["tp-connected==0.0.4"] + "requirements": [] } diff --git a/homeassistant/components/tplink_lte/notify.py b/homeassistant/components/tplink_lte/notify.py deleted file mode 100644 index 674f09efcd7..00000000000 --- a/homeassistant/components/tplink_lte/notify.py +++ /dev/null @@ -1,55 +0,0 @@ -"""Support for TP-Link LTE notifications.""" - -from __future__ import annotations - -import logging -from typing import Any - -import attr -import tp_connected - -from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService -from homeassistant.const import CONF_RECIPIENT -from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType - -from . import DATA_KEY, LTEData - -_LOGGER = logging.getLogger(__name__) - - -async def async_get_service( - hass: HomeAssistant, - config: ConfigType, - discovery_info: DiscoveryInfoType | None = None, -) -> TplinkNotifyService | None: - """Get the notification service.""" - if discovery_info is None: - return None - return TplinkNotifyService(hass, discovery_info) - - -@attr.s -class TplinkNotifyService(BaseNotificationService): - """Implementation of a notification service.""" - - hass: HomeAssistant = attr.ib() - config: dict[str, Any] = attr.ib() - - async def async_send_message(self, message: str = "", **kwargs: Any) -> None: - """Send a message to a user.""" - - lte_data: LTEData = self.hass.data[DATA_KEY] - modem_data = lte_data.get_modem_data(self.config) - if not modem_data: - _LOGGER.error("No modem available") - return - - phone = self.config[CONF_RECIPIENT] - targets = kwargs.get(ATTR_TARGET, phone) - if targets and message: - for target in targets: - try: - await modem_data.modem.sms(target, message) - except tp_connected.Error: - _LOGGER.error("Unable to send to %s", target) diff --git a/homeassistant/components/tplink_lte/strings.json b/homeassistant/components/tplink_lte/strings.json new file mode 100644 index 00000000000..d03b650746f --- /dev/null +++ b/homeassistant/components/tplink_lte/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "integration_removed": { + "description": "The TP-Link LTE integration has been removed from Home Assistant.\n\nThe integration has not been working since Home Assistant 2023.6.0, has no maintainer, and its underlying library depends on a package with a [critical security vulnerability]({ghsa_url}).\n\nTo resolve this issue, remove the `tplink_lte` configuration from your `configuration.yaml` file and restart Home Assistant.", + "title": "The TP-Link LTE integration has been removed" + } + } +} diff --git a/requirements_all.txt b/requirements_all.txt index 14cf7528970..ba45928ccba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3138,9 +3138,6 @@ toonapi==0.3.0 # homeassistant.components.totalconnect total-connect-client==2025.12.2 -# homeassistant.components.tplink_lte -tp-connected==0.0.4 - # homeassistant.components.tplink_omada tplink-omada-client==1.5.6 diff --git a/tests/components/tplink_lte/__init__.py b/tests/components/tplink_lte/__init__.py new file mode 100644 index 00000000000..d487e0eba93 --- /dev/null +++ b/tests/components/tplink_lte/__init__.py @@ -0,0 +1 @@ +"""Tests for the TP-Link LTE integration.""" diff --git a/tests/components/tplink_lte/test_init.py b/tests/components/tplink_lte/test_init.py new file mode 100644 index 00000000000..76f65d83b8d --- /dev/null +++ b/tests/components/tplink_lte/test_init.py @@ -0,0 +1,23 @@ +"""Tests for the TP-Link LTE integration.""" + +from homeassistant.components.tplink_lte import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir +from homeassistant.setup import async_setup_component + + +async def test_tplink_lte_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the TP-Link LTE repair issue is created on setup.""" + assert await async_setup_component( + hass, + DOMAIN, + {DOMAIN: [{"host": "192.168.0.1", "password": "secret"}]}, + ) + await hass.async_block_till_done() + + assert issue_registry.async_get_issue(DOMAIN, DOMAIN) + issue = issue_registry.async_get_issue(DOMAIN, DOMAIN) + assert issue.severity == ir.IssueSeverity.ERROR + assert issue.translation_key == "integration_removed"