diff --git a/.coveragerc b/.coveragerc index 1ceca701d50afcb4385673118d59486f889e9283..2e5748d2a98d87f9e143d3815327752a3ae23bb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -804,7 +804,8 @@ omit = homeassistant/components/netgear/sensor.py homeassistant/components/netgear/switch.py homeassistant/components/netgear/update.py - homeassistant/components/netgear_lte/* + homeassistant/components/netgear_lte/__init__.py + homeassistant/components/netgear_lte/notify.py homeassistant/components/netio/switch.py homeassistant/components/neurio_energy/sensor.py homeassistant/components/nexia/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index e664d89a028ce046c052e144645214f5c7860389..fda7f27c41220e287b4cfd31822161b3b2fe0186 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -847,6 +847,7 @@ build.json @home-assistant/supervisor /homeassistant/components/netgear/ @hacf-fr @Quentame @starkillerOG /tests/components/netgear/ @hacf-fr @Quentame @starkillerOG /homeassistant/components/netgear_lte/ @tkdrob +/tests/components/netgear_lte/ @tkdrob /homeassistant/components/network/ @home-assistant/core /tests/components/network/ @home-assistant/core /homeassistant/components/nexia/ @bdraco diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index d6ce3cb09944d8ea2346b61c2b9cfbc4e7319957..c7dd21405559fd266dfc63952daa8011ec7877a0 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -1,12 +1,12 @@ """Support for Netgear LTE modems.""" -import asyncio from datetime import timedelta -import aiohttp +from aiohttp.cookiejar import CookieJar import attr import eternalegypt import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, @@ -16,11 +16,13 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from . import sensor_types @@ -32,6 +34,7 @@ from .const import ( CONF_BINARY_SENSOR, CONF_NOTIFY, CONF_SENSOR, + DATA_HASS_CONFIG, DISPATCHER_NETGEAR_LTE, DOMAIN, LOGGER, @@ -90,6 +93,12 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.NOTIFY, + Platform.SENSOR, +] + @attr.s class ModemData: @@ -137,90 +146,108 @@ class LTEData: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Netgear LTE component.""" - if DOMAIN not in hass.data: - websession = async_create_clientsession( - hass, cookie_jar=aiohttp.CookieJar(unsafe=True) - ) - hass.data[DOMAIN] = LTEData(websession) + hass.data[DATA_HASS_CONFIG] = config - await async_setup_services(hass) + if lte_config := config.get(DOMAIN): + await hass.async_create_task(import_yaml(hass, lte_config)) - netgear_lte_config = config[DOMAIN] + return True - # Set up each modem - tasks = [ - hass.async_create_task(_setup_lte(hass, lte_conf)) - for lte_conf in netgear_lte_config - ] - await asyncio.wait(tasks) - - # Load platforms for each modem - for lte_conf in netgear_lte_config: - # Notify - for notify_conf in lte_conf[CONF_NOTIFY]: - discovery_info = { - CONF_HOST: lte_conf[CONF_HOST], - CONF_NAME: notify_conf.get(CONF_NAME), - CONF_NOTIFY: notify_conf, - } - hass.async_create_task( - discovery.async_load_platform( - hass, Platform.NOTIFY, DOMAIN, discovery_info, config - ) - ) - # Sensor - sensor_conf = lte_conf[CONF_SENSOR] - discovery_info = {CONF_HOST: lte_conf[CONF_HOST], CONF_SENSOR: sensor_conf} - hass.async_create_task( - discovery.async_load_platform( - hass, Platform.SENSOR, DOMAIN, discovery_info, config - ) +async def import_yaml(hass: HomeAssistant, lte_config: ConfigType) -> None: + """Import yaml if we can connect. Create appropriate issue registry entries.""" + for entry in lte_config: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry ) - - # Binary Sensor - binary_sensor_conf = lte_conf[CONF_BINARY_SENSOR] - discovery_info = { - CONF_HOST: lte_conf[CONF_HOST], - CONF_BINARY_SENSOR: binary_sensor_conf, - } - hass.async_create_task( - discovery.async_load_platform( - hass, Platform.BINARY_SENSOR, DOMAIN, discovery_info, config + if result.get("reason") == "cannot_connect": + async_create_issue( + hass, + DOMAIN, + "import_failure", + is_fixable=False, + severity=IssueSeverity.ERROR, + translation_key="import_failure", + ) + else: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Netgear LTE", + }, ) - ) - - return True -async def _setup_lte(hass, lte_config): - """Set up a Netgear LTE modem.""" +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Netgear LTE from a config entry.""" + host = entry.data[CONF_HOST] + password = entry.data[CONF_PASSWORD] - host = lte_config[CONF_HOST] - password = lte_config[CONF_PASSWORD] + if DOMAIN not in hass.data: + websession = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) - websession = hass.data[DOMAIN].websession - modem = eternalegypt.Modem(hostname=host, websession=websession) + hass.data[DOMAIN] = LTEData(websession) + modem = eternalegypt.Modem(hostname=host, websession=hass.data[DOMAIN].websession) modem_data = ModemData(hass, host, modem) - try: - await _login(hass, modem_data, password) - except eternalegypt.Error: - retry_task = hass.loop.create_task(_retry_login(hass, modem_data, password)) + await _login(hass, modem_data, password) + + async def _update(now): + """Periodic update.""" + await modem_data.async_update() + + update_unsub = async_track_time_interval(hass, _update, SCAN_INTERVAL) + + async def cleanup(event: Event | None = None) -> None: + """Clean up resources.""" + update_unsub() + await modem.logout() + if DOMAIN in hass.data: + del hass.data[DOMAIN].modem_data[modem_data.host] + + entry.async_on_unload(cleanup) + entry.async_on_unload(hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)) - @callback - def cleanup_retry(event): - """Clean up retry task resources.""" - if not retry_task.done(): - retry_task.cancel() + await async_setup_services(hass) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_retry) + _legacy_task(hass, entry) + await hass.config_entries.async_forward_entry_setups( + entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] + ) -async def _login(hass, modem_data, password): + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + hass.data.pop(DOMAIN) + + return unload_ok + + +async def _login(hass: HomeAssistant, modem_data: ModemData, password: str) -> None: """Log in and complete setup.""" - await modem_data.modem.login(password=password) + try: + await modem_data.modem.login(password=password) + except eternalegypt.Error as ex: + raise ConfigEntryNotReady("Cannot connect/authenticate") from ex def fire_sms_event(sms): """Send an SMS event.""" @@ -237,33 +264,63 @@ async def _login(hass, modem_data, password): await modem_data.async_update() hass.data[DOMAIN].modem_data[modem_data.host] = modem_data - async def _update(now): - """Periodic update.""" - await modem_data.async_update() - update_unsub = async_track_time_interval(hass, _update, SCAN_INTERVAL) - - async def cleanup(event): - """Clean up resources.""" - update_unsub() - await modem_data.modem.logout() - del hass.data[DOMAIN].modem_data[modem_data.host] - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) - - -async def _retry_login(hass, modem_data, password): - """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) - except eternalegypt.Error: - delay = min(2 * delay, 300) +def _legacy_task(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Create notify service and add a repair issue when appropriate.""" + # Discovery can happen up to 2 times for notify depending on existing yaml config + # One for the name of the config entry, allows the user to customize the name + # One for each notify described in the yaml config which goes away with config flow + # One for the default if the user never specified one + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_HOST: entry.data[CONF_HOST], CONF_NAME: entry.title}, + hass.data[DATA_HASS_CONFIG], + ) + ) + if not (lte_configs := hass.data[DATA_HASS_CONFIG].get(DOMAIN, [])): + return + async_create_issue( + hass, + DOMAIN, + "deprecated_notify", + breaks_in_ha_version="2024.7.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_notify", + translation_placeholders={ + "name": f"{Platform.NOTIFY}.{entry.title.lower().replace(' ', '_')}" + }, + ) + + for lte_config in lte_configs: + if lte_config[CONF_HOST] == entry.data[CONF_HOST]: + if not lte_config[CONF_NOTIFY]: + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {CONF_HOST: entry.data[CONF_HOST], CONF_NAME: DOMAIN}, + hass.data[DATA_HASS_CONFIG], + ) + ) + break + for notify_conf in lte_config[CONF_NOTIFY]: + discovery_info = { + CONF_HOST: lte_config[CONF_HOST], + CONF_NAME: notify_conf.get(CONF_NAME), + CONF_NOTIFY: notify_conf, + } + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + discovery_info, + hass.data[DATA_HASS_CONFIG], + ) + ) + break diff --git a/homeassistant/components/netgear_lte/binary_sensor.py b/homeassistant/components/netgear_lte/binary_sensor.py index add59096024ce2db421c3c0facee0ee5bcc5f419..b8441d8fb7cb136a0eca78eafee54390f35afbe3 100644 --- a/homeassistant/components/netgear_lte/binary_sensor.py +++ b/homeassistant/components/netgear_lte/binary_sensor.py @@ -2,40 +2,24 @@ from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_BINARY_SENSOR, DOMAIN +from .const import DOMAIN from .entity import LTEEntity -from .sensor_types import BINARY_SENSOR_CLASSES +from .sensor_types import ALL_BINARY_SENSORS, BINARY_SENSOR_CLASSES -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Netgear LTE binary sensor devices.""" - if discovery_info is None: - return + """Set up the Netgear LTE binary sensor.""" + modem_data = hass.data[DOMAIN].get_modem_data(entry.data) - modem_data = hass.data[DOMAIN].get_modem_data(discovery_info) - - if not modem_data or not modem_data.data: - raise PlatformNotReady - - binary_sensor_conf = discovery_info[CONF_BINARY_SENSOR] - monitored_conditions = binary_sensor_conf[CONF_MONITORED_CONDITIONS] - - binary_sensors = [] - for sensor_type in monitored_conditions: - binary_sensors.append(LTEBinarySensor(modem_data, sensor_type)) - - async_add_entities(binary_sensors) + async_add_entities( + LTEBinarySensor(modem_data, sensor) for sensor in ALL_BINARY_SENSORS + ) class LTEBinarySensor(LTEEntity, BinarySensorEntity): diff --git a/homeassistant/components/netgear_lte/config_flow.py b/homeassistant/components/netgear_lte/config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..a3a56bab03bd933a2c735e65d3a32dfb5c54f002 --- /dev/null +++ b/homeassistant/components/netgear_lte/config_flow.py @@ -0,0 +1,103 @@ +"""Config flow for Netgear LTE integration.""" +from __future__ import annotations + +from typing import Any + +from aiohttp.cookiejar import CookieJar +from eternalegypt import Error, Modem +from eternalegypt.eternalegypt import Information +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_create_clientsession + +from .const import DEFAULT_HOST, DOMAIN, LOGGER, MANUFACTURER + + +class NetgearLTEFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Netgear LTE.""" + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a configuration from config.yaml.""" + host = config[CONF_HOST] + password = config[CONF_PASSWORD] + self._async_abort_entries_match({CONF_HOST: host}) + try: + info = await self._async_validate_input(host, password) + except InputValidationError: + return self.async_abort(reason="cannot_connect") + await self.async_set_unique_id(info.serial_number) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{MANUFACTURER} {info.items['general.devicename']}", + data={CONF_HOST: host, CONF_PASSWORD: password}, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + + if user_input: + host = user_input[CONF_HOST] + password = user_input[CONF_PASSWORD] + + try: + info = await self._async_validate_input(host, password) + except InputValidationError as ex: + errors["base"] = ex.base + else: + await self.async_set_unique_id(info.serial_number) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{MANUFACTURER} {info.items['general.devicename']}", + data={CONF_HOST: host, CONF_PASSWORD: password}, + ) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PASSWORD): str, + } + ), + user_input or {CONF_HOST: DEFAULT_HOST}, + ), + errors=errors, + ) + + async def _async_validate_input(self, host: str, password: str) -> Information: + """Validate login credentials.""" + websession = async_create_clientsession( + self.hass, cookie_jar=CookieJar(unsafe=True) + ) + + modem = Modem( + hostname=host, + password=password, + websession=websession, + ) + try: + await modem.login() + info = await modem.information() + except Error as ex: + raise InputValidationError("cannot_connect") from ex + except Exception as ex: + LOGGER.exception("Unexpected exception") + raise InputValidationError("unknown") from ex + await modem.logout() + return info + + +class InputValidationError(exceptions.HomeAssistantError): + """Error to indicate we cannot proceed due to invalid input.""" + + def __init__(self, base: str) -> None: + """Initialize with error base.""" + super().__init__() + self.base = base diff --git a/homeassistant/components/netgear_lte/const.py b/homeassistant/components/netgear_lte/const.py index 12c8f06b695c037db83c55128efd12a958839252..b47218bf4e107aa84371adad525e23ffb08b3213 100644 --- a/homeassistant/components/netgear_lte/const.py +++ b/homeassistant/components/netgear_lte/const.py @@ -14,9 +14,14 @@ CONF_BINARY_SENSOR: Final = "binary_sensor" CONF_NOTIFY: Final = "notify" CONF_SENSOR: Final = "sensor" +DATA_HASS_CONFIG = "netgear_lte_hass_config" +# https://kb.netgear.com/31160/How-do-I-change-my-4G-LTE-Modem-s-IP-address-range +DEFAULT_HOST = "192.168.5.1" DISPATCHER_NETGEAR_LTE = "netgear_lte_update" DOMAIN: Final = "netgear_lte" FAILOVER_MODES = ["auto", "wire", "mobile"] LOGGER = logging.getLogger(__package__) + +MANUFACTURER: Final = "Netgear" diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index c9a5245da4155b3a882d62a09d581b6d75b5b728..bc103018359cb3c181b75c020d76732e1fb505b2 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -2,6 +2,7 @@ "domain": "netgear_lte", "name": "NETGEAR LTE", "codeowners": ["@tkdrob"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/netgear_lte", "iot_class": "local_polling", "loggers": ["eternalegypt"], diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index c21b56799eb7586323ea41260d11969b7c35d38c..ddc5e93677cb77c840054cf91cc4b2fe743f5e99 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -38,8 +38,8 @@ class NetgearNotifyService(BaseNotificationService): if not modem_data: LOGGER.error("Modem not ready") return - - targets = kwargs.get(ATTR_TARGET, self.config[CONF_NOTIFY][CONF_RECIPIENT]) + if not (targets := kwargs.get(ATTR_TARGET)): + targets = self.config[CONF_NOTIFY][CONF_RECIPIENT] if not targets: LOGGER.warning("No recipients") return diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index 4ca127e572417517ccb16b8bafad68faa0188a51..5632999ae96454932b1f64cdec620631c92929e8 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -2,45 +2,37 @@ from __future__ import annotations from homeassistant.components.sensor import SensorDeviceClass, SensorEntity -from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_SENSOR, DOMAIN +from .const import DOMAIN from .entity import LTEEntity -from .sensor_types import SENSOR_SMS, SENSOR_SMS_TOTAL, SENSOR_UNITS, SENSOR_USAGE +from .sensor_types import ( + ALL_SENSORS, + SENSOR_SMS, + SENSOR_SMS_TOTAL, + SENSOR_UNITS, + SENSOR_USAGE, +) -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up Netgear LTE sensor devices.""" - if discovery_info is None: - return - - modem_data = hass.data[DOMAIN].get_modem_data(discovery_info) - - if not modem_data or not modem_data.data: - raise PlatformNotReady - - sensor_conf = discovery_info[CONF_SENSOR] - monitored_conditions = sensor_conf[CONF_MONITORED_CONDITIONS] + """Set up the Netgear LTE sensor.""" + modem_data = hass.data[DOMAIN].get_modem_data(entry.data) sensors: list[SensorEntity] = [] - for sensor_type in monitored_conditions: - if sensor_type == SENSOR_SMS: - sensors.append(SMSUnreadSensor(modem_data, sensor_type)) - elif sensor_type == SENSOR_SMS_TOTAL: - sensors.append(SMSTotalSensor(modem_data, sensor_type)) - elif sensor_type == SENSOR_USAGE: - sensors.append(UsageSensor(modem_data, sensor_type)) + for sensor in ALL_SENSORS: + if sensor == SENSOR_SMS: + sensors.append(SMSUnreadSensor(modem_data, sensor)) + elif sensor == SENSOR_SMS_TOTAL: + sensors.append(SMSTotalSensor(modem_data, sensor)) + elif sensor == SENSOR_USAGE: + sensors.append(UsageSensor(modem_data, sensor)) else: - sensors.append(GenericSensor(modem_data, sensor_type)) + sensors.append(GenericSensor(modem_data, sensor)) async_add_entities(sensors) diff --git a/homeassistant/components/netgear_lte/strings.json b/homeassistant/components/netgear_lte/strings.json index 1fd1028299140bf36bbad39453a7f494b06662a4..8992fb50670ff8cfbbb163e67131c098a2338876 100644 --- a/homeassistant/components/netgear_lte/strings.json +++ b/homeassistant/components/netgear_lte/strings.json @@ -1,4 +1,32 @@ { + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "issues": { + "deprecated_notify": { + "title": "The Netgear LTE notify service is changing", + "description": "The Netgear LTE notify service was previously set up via YAML configuration.\n\nThis created a service for a specified recipient without having to include the phone number.\n\nPlease adjust any automations or scripts you may have to use the `{name}` service and include target for specifying a recipient." + }, + "import_failure": { + "title": "The Netgear LTE integration failed to import", + "description": "The Netgear LTE notify service was previously set up via YAML configuration.\n\nAn error occurred when trying to communicate with the device while attempting to import the configuration to the UI.\n\nPlease remove the Netgear LTE notify section from your YAML configuration and set it up in the UI instead." + } + }, "services": { "delete_sms": { "name": "Delete SMS", @@ -52,6 +80,5 @@ } } } - }, - "selector": {} + } } diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9274593b86f4f160341aec2d049d245d0e65e205..dded01474228b452cfcfe5a72bda019860459a5f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -319,6 +319,7 @@ FLOWS = { "nest", "netatmo", "netgear", + "netgear_lte", "nexia", "nextbus", "nextcloud", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9bd3de30b2964d7fefbc105fb28bb8b3ad38c22d..d80f4f18925de0c6d584d4be41f9fb221d9f001a 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3791,7 +3791,7 @@ }, "netgear_lte": { "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling", "name": "NETGEAR LTE" } diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2a3dffbe4100d5f271bfb0a2ff965e88ea3cfbe..9c97dce0a8be54b378dbf2e3e508d63934757c09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -641,6 +641,9 @@ epson-projector==0.5.1 # homeassistant.components.esphome esphome-dashboard-api==1.2.3 +# homeassistant.components.netgear_lte +eternalegypt==0.0.16 + # homeassistant.components.eufylife_ble eufylife-ble-client==0.1.8 diff --git a/tests/components/netgear_lte/__init__.py b/tests/components/netgear_lte/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6661c92312e0b46908ce1f028abb0aa5266106eb --- /dev/null +++ b/tests/components/netgear_lte/__init__.py @@ -0,0 +1 @@ +"""Tests for the Netgear LTE component.""" diff --git a/tests/components/netgear_lte/conftest.py b/tests/components/netgear_lte/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..e32034d660b9f9d84c515568d74a57d0369ee988 --- /dev/null +++ b/tests/components/netgear_lte/conftest.py @@ -0,0 +1,85 @@ +"""Configure pytest for Netgear LTE tests.""" +from __future__ import annotations + +from aiohttp.client_exceptions import ClientError +import pytest + +from homeassistant.components.netgear_lte.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONTENT_TYPE_JSON +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +HOST = "192.168.5.1" +PASSWORD = "password" + +CONF_DATA = {CONF_HOST: HOST, CONF_PASSWORD: PASSWORD} + + +@pytest.fixture +def cannot_connect(aioclient_mock: AiohttpClientMocker) -> None: + """Mock cannot connect error.""" + aioclient_mock.get(f"http://{HOST}/model.json", exc=ClientError) + aioclient_mock.post(f"http://{HOST}/Forms/config", exc=ClientError) + + +@pytest.fixture +def unknown(aioclient_mock: AiohttpClientMocker) -> None: + """Mock Netgear LTE unknown error.""" + aioclient_mock.get( + f"http://{HOST}/model.json", + text="something went wrong", + headers={"Content-Type": "application/javascript"}, + ) + + +@pytest.fixture(name="connection") +def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: + """Mock Netgear LTE connection.""" + aioclient_mock.get( + f"http://{HOST}/model.json", + text=load_fixture("netgear_lte/model.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.post( + f"http://{HOST}/Forms/config", + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + aioclient_mock.post( + f"http://{HOST}/Forms/smsSendMsg", + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + +@pytest.fixture(name="config_entry") +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create Netgear LTE entry in Home Assistant.""" + return MockConfigEntry( + domain=DOMAIN, data=CONF_DATA, unique_id="FFFFFFFFFFFFF", title="Netgear LM1200" + ) + + +@pytest.fixture(name="setup_integration") +async def mock_setup_integration( + hass: HomeAssistant, + config_entry: MockConfigEntry, + connection: None, +) -> None: + """Set up the Netgear LTE integration in Home Assistant.""" + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + +@pytest.fixture(name="setup_cannot_connect") +async def setup_cannot_connect( + hass: HomeAssistant, + config_entry: MockConfigEntry, + cannot_connect: None, +) -> None: + """Set up the Netgear LTE integration in Home Assistant.""" + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() diff --git a/tests/components/netgear_lte/fixtures/model.json b/tests/components/netgear_lte/fixtures/model.json new file mode 100644 index 0000000000000000000000000000000000000000..c5f4a13f3afa814b93dc14a123c09ad695c71a6f --- /dev/null +++ b/tests/components/netgear_lte/fixtures/model.json @@ -0,0 +1,450 @@ +{ + "custom": { "AtTcpEnable": false, "end": 0 }, + "webd": { + "adminPassword": "****************", + "ownerModeEnabled": false, + "hideAdminPassword": true, + "end": "" + }, + "lcd": { "end": "" }, + "sim": { + "pin": { "mode": "Disabled", "retry": 3, "end": "" }, + "puk": { "retry": 10 }, + "mep": {}, + "phoneNumber": "(555) 555-5555", + "iccid": "1234567890123456789", + "imsi": "123456789012345", + "SPN": "", + "status": "Ready", + "end": "" + }, + "sms": { + "ready": true, + "sendEnabled": true, + "sendSupported": true, + "alertSupported": true, + "alertEnabled": false, + "alertNumList": "", + "alertCfgList": [ + { "category": "FWUpdate", "enabled": false }, + { "category": "DataUsageWarning", "enabled": false }, + { "category": "DataUsageExceeded", "enabled": false }, + { "category": "LTEFailoverLTE", "enabled": false }, + { "category": "LTEFailoverETH", "enabled": false }, + {} + ], + "unreadMsgs": 1, + "msgCount": 1, + "msgs": [ + { + "id": "1", + "rxTime": "20/01/23 03:39:35 PM", + "text": "text", + "sender": "889", + "read": false + }, + {} + ], + "trans": [{}], + "sendMsg": [ + { + "clientId": "eternalegypt.eternalegypt", + "enc": "Gsm7Bit", + "errorCode": 0, + "msgId": 1, + "receiver": "+15555555555", + "status": "Succeeded", + "text": "test SMS from Home Assistant", + "txTime": "1367252824" + }, + {} + ], + "end": "" + }, + "session": { + "userRole": "Admin", + "lang": "en", + "secToken": "secret" + }, + "general": { + "defaultLanguage": "en", + "PRIid": "12345678", + "genericResetStatus": "NotStarted", + "manufacturer": "Netgear", + "model": "LM1200", + "HWversion": "1.0", + "FWversion": "EC25AFFDR07A09M4G", + "appVersion": "NTG9X07C_20.06.09.00", + "buildDate": "Unknown", + "BLversion": "", + "PRIversion": "04.19", + "IMEI": "123456789012345", + "SVN": "9", + "MEID": "", + "ESN": "0", + "FSN": "FFFFFFFFFFFFF", + "activated": true, + "webAppVersion": "LM1200-HDATA_03.03.103.201", + "HIDenabled": false, + "TCAaccepted": true, + "LEDenabled": true, + "showAdvHelp": true, + "keyLockState": "Unlocked", + "devTemperature": 30, + "verMajor": 1000, + "verMinor": 0, + "environment": "Application", + "currTime": 1367257216, + "timeZoneOffset": -14400, + "deviceName": "LM1200", + "useMetricSystem": true, + "factoryResetStatus": "NotStarted", + "setupCompleted": true, + "languageSelected": false, + "systemAlertList": { "list": [{}], "count": 0 }, + "apiVersion": "2.0", + "companyName": "NETGEAR", + "configURL": "/Forms/config", + "profileURL": "/Forms/profile", + "pinChangeURL": "/Forms/pinChange", + "portCfgURL": "/Forms/portCfg", + "portFilterURL": "/Forms/portFilter", + "wifiACLURL": "/Forms/wifiACL", + "supportedLangList": [ + { + "id": "en", + "isCurrent": "true", + "isDefault": "true", + "label": "English", + "token1": "/romfs/lcd/en_us.tr", + "token2": "" + }, + { + "id": "de_DE", + "isCurrent": "false", + "isDefault": "false", + "label": "Deutsch (German)", + "token1": "/romfs/lcd/de_de.tr", + "token2": "" + }, + { + "id": "ar_AR", + "isCurrent": "false", + "isDefault": "false", + "label": "العربية (Arabic)", + "token1": "/romfs/lcd/ar_AR.tr", + "token2": "" + }, + { + "id": "es_ES", + "isCurrent": "false", + "isDefault": "false", + "label": "Español (Spanish)", + "token1": "/romfs/lcd/es_es.tr", + "token2": "" + }, + { + "id": "fr_FR", + "isCurrent": "false", + "isDefault": "false", + "label": "Français (French)", + "token1": "/romfs/lcd/fr_fr.tr", + "token2": "" + }, + { + "id": "it_IT", + "isCurrent": "false", + "isDefault": "false", + "label": "Italiano (Italian)", + "token1": "/romfs/lcd/it_it.tr", + "token2": "" + }, + { + "id": "pl_PL", + "isCurrent": "false", + "isDefault": "false", + "label": "Polski (Polish)", + "token1": "/romfs/lcd/pl_pl.tr", + "token2": "" + }, + { + "id": "fi_FI", + "isCurrent": "false", + "isDefault": "false", + "label": "Suomi (Finnish)", + "token1": "/romfs/lcd/fi_fi.tr", + "token2": "" + }, + { + "id": "sv_SE", + "isCurrent": "false", + "isDefault": "false", + "label": "Svenska (Swedish)", + "token1": "/romfs/lcd/sv_se.tr", + "token2": "" + }, + { + "id": "tu_TU", + "isCurrent": "false", + "isDefault": "false", + "label": "Türkçe (Turkish)", + "token1": "/romfs/lcd/tu_tu.tr", + "token2": "" + }, + {} + ] + }, + "power": { + "PMState": "Init", + "SmState": "Online", + "autoOff": { + "onUSBdisconnect": { "enable": false, "countdownTimer": 0, "end": "" }, + "onIdle": { "timer": { "onAC": 0, "onBattery": 0, "end": "" } } + }, + "standby": { + "onIdle": { + "timer": { "onAC": 0, "onBattery": 600, "onUSB": 0, "end": "" } + } + }, + "autoOn": { "enable": true, "end": "" }, + "buttonHoldTime": 3, + "deviceTempCritical": false, + "resetreason": 16, + "resetRequired": "NoResetRequired", + "lpm": false, + "end": "" + }, + "wwan": { + "netScanStatus": "NotStarted", + "inactivityCause": 307, + "currentNWserviceType": "LteService", + "registerRejectCode": 0, + "netSelEnabled": "Enabled", + "netRegMode": "Auto", + "IPv6": "1234:abcd::1234:abcd", + "roaming": false, + "IP": "10.0.0.5", + "registerNetworkDisplay": "T-Mobile", + "RAT": "Only4G", + "bandRegion": [ + { "index": 0, "name": "Auto", "current": false }, + { "index": 1, "name": "LTE Only", "current": true }, + { "index": 2, "name": "WCDMA Only", "current": false }, + {} + ], + "autoconnect": "HomeNetwork", + "profileList": [ + { + "index": 1, + "id": "T-Mobile 9", + "name": "T Mobile", + "apn": "fast.t-mobile.com", + "username": "", + "password": "", + "authtype": "None", + "ipaddr": "0.0.0.0", + "type": "IPV4V6", + "pdproamingtype": "IPV4" + }, + { + "index": 2, + "id": "Mint", + "name": "Mint", + "apn": "wholesale", + "username": "", + "password": "", + "authtype": "None", + "ipaddr": "0.0.0.0", + "type": "IPV4V6", + "pdproamingtype": "IPV4" + }, + {} + ], + "profile": { + "default": "T-Mobile 9", + "defaultLTE": "T-Mobile 9", + "full": false, + "promptForApnSelection": false, + "end": "" + }, + "dataUsage": { + "total": { + "lteBillingTx": 0, + "lteBillingRx": 0, + "cdmaBillingTx": 0, + "cdmaBillingRx": 0, + "gwBillingTx": 0, + "gwBillingRx": 0, + "lteLifeTx": 0, + "lteLifeRx": 0, + "cdmaLifeTx": 0, + "cdmaLifeRx": 0, + "gwLifeTx": 0, + "gwLifeRx": 0, + "end": "" + }, + "server": { "accountType": "", "subAccountType": "", "end": "" }, + "serverDataRemaining": 0, + "serverDataTransferred": 0, + "serverDataTransferredIntl": 0, + "serverDataValidState": "Invalid", + "serverDaysLeft": 0, + "serverErrorCode": "", + "serverLowBalance": false, + "serverMsisdn": "", + "serverRechargeUrl": "", + "dataWarnEnable": true, + "prepaidAccountState": "Hot", + "accountType": "Unknown", + "share": { + "enabled": false, + "dataTransferredOthers": 0, + "lastSync": "0", + "end": "" + }, + "generic": { + "dataLimitValid": false, + "usageHighWarning": 80, + "lastSucceeded": "0", + "billingDay": 1, + "nextBillingDate": "1369627200", + "lastSync": "0", + "billingCycleRemainder": 27, + "billingCycleLimit": 0, + "dataTransferred": 42484315, + "dataTransferredRoaming": 0, + "lastReset": "1366948800", + "end": "" + } + }, + "netManualNoCvg": false, + "connection": "Connected", + "connectionType": "IPv4AndIPv6", + "currentPSserviceType": "LTE", + "ca": { "end": "" }, + "connectionText": "4G", + "sessDuration": 4282, + "sessStartTime": 1367252934, + "dataTransferred": { "totalb": "345036", "rxb": "184700", "txb": "160336" }, + "signalStrength": { + "rssi": 0, + "rscp": 0, + "ecio": 0, + "rsrp": -113, + "rsrq": -20, + "bars": 2, + "sinr": 0, + "end": "" + } + }, + "wwanadv": { + "curBand": "LTE B4", + "radioQuality": 52, + "country": "USA", + "RAC": 0, + "LAC": 12345, + "MCC": "123", + "MNC": "456", + "MNCFmt": 3, + "cellId": 12345678, + "chanId": 2300, + "primScode": -1, + "plmnSrvErrBitMask": 0, + "chanIdUl": 20300, + "txLevel": 4, + "rxLevel": -113, + "end": "" + }, + "ethernet": { + "offload": { "ipv4Addr": "0.0.0.0", "ipv6Addr": "", "end": "" } + }, + "wifi": { + "enabled": true, + "maxClientSupported": 0, + "maxClientLimit": 0, + "maxClientCnt": 0, + "channel": 0, + "hiddenSSID": true, + "passPhrase": "", + "RTSthreshold": 0, + "fragThreshold": 0, + "SSID": "", + "clientCount": 0, + "country": "", + "wps": { "supported": "Disabled", "end": "" }, + "guest": { + "maxClientCnt": 0, + "enabled": false, + "SSID": "", + "passPhrase": "", + "generatePassphrase": false, + "hiddenSSID": true, + "chan": 0, + "DHCP": { "range": { "end": "" } } + }, + "offload": { "end": "" }, + "end": "" + }, + "router": { + "gatewayIP": "192.168.5.1", + "DMZaddress": "192.168.5.4", + "DMZenabled": false, + "forceSetup": false, + "DHCP": { + "serverEnabled": true, + "DNS1": "1.1.1.1", + "DNS2": "1.1.2.2", + "DNSmode": "Auto", + "USBpcIP": "0.0.0.0", + "leaseTime": 43200, + "range": { "high": "192.168.5.99", "low": "192.168.5.20", "end": "" } + }, + "usbMode": "None", + "usbNetworkTethering": true, + "portFwdEnabled": false, + "portFwdList": [{}], + "portFilteringEnabled": false, + "portFilteringMode": "None", + "portFilterWhiteList": [{}], + "portFilterBlackList": [{}], + "hostName": "routerlogin", + "domainName": "net", + "ipPassThroughEnabled": false, + "ipPassThroughSupported": true, + "Ipv6Supported": true, + "UPNPsupported": false, + "UPNPenabled": false, + "clientList": { "list": [{}], "count": 0 }, + "end": "" + }, + "fota": { + "fwupdater": { + "available": false, + "chkallow": true, + "chkstatus": "Initial", + "dloadProg": 0, + "error": false, + "lastChkDate": 1367200419, + "state": "NoNewFw", + "isPostponable": false, + "statusCode": 200, + "chkTimeLeft": 0, + "dloadSize": 0, + "end": "" + } + }, + "failover": { + "mode": "Auto", + "backhaul": "LTE", + "supported": true, + "monitorPeriod": 10, + "wanConnected": false, + "keepaliveEnable": false, + "keepaliveSleep": 15, + "ipv4Targets": [{ "id": "0", "string": "8.8.8.8" }, {}], + "ipv6Targets": [{}], + "end": "" + }, + "eventlog": { "level": 0, "end": 0 }, + "ui": { "serverDaysLeftHide": false, "promptActivation": true, "end": 0 } +} diff --git a/tests/components/netgear_lte/test_binary_sensor.py b/tests/components/netgear_lte/test_binary_sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..8ed43c8c887bea33758fc2970a60f901530434dc --- /dev/null +++ b/tests/components/netgear_lte/test_binary_sensor.py @@ -0,0 +1,19 @@ +"""The tests for Netgear LTE binary sensor platform.""" +import pytest + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant + + +@pytest.mark.usefixtures("setup_integration", "entity_registry_enabled_by_default") +async def test_binary_sensors(hass: HomeAssistant) -> None: + """Test for successfully setting up the Netgear LTE binary sensor platform.""" + state = hass.states.get("binary_sensor.netgear_lte_mobile_connected") + assert state.state == STATE_ON + assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.CONNECTIVITY + state = hass.states.get("binary_sensor.netgear_lte_wire_connected") + assert state.state == STATE_OFF + assert state.attributes[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.CONNECTIVITY + state = hass.states.get("binary_sensor.netgear_lte_roaming") + assert state.state == STATE_OFF diff --git a/tests/components/netgear_lte/test_config_flow.py b/tests/components/netgear_lte/test_config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..97a624a14e7c081c3bf67b7ae8826c89ee322901 --- /dev/null +++ b/tests/components/netgear_lte/test_config_flow.py @@ -0,0 +1,110 @@ +"""Test Netgear LTE config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.netgear_lte.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_SOURCE +from homeassistant.core import HomeAssistant + +from .conftest import CONF_DATA + + +def _patch_setup(): + return patch( + "homeassistant.components.netgear_lte.async_setup_entry", return_value=True + ) + + +async def test_flow_user_form(hass: HomeAssistant, connection: None) -> None: + """Test that the user set up form is served.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with _patch_setup(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Netgear LM1200" + assert result["data"] == CONF_DATA + assert result["context"]["unique_id"] == "FFFFFFFFFFFFF" + + +@pytest.mark.parametrize("source", (SOURCE_USER, SOURCE_IMPORT)) +async def test_flow_already_configured( + hass: HomeAssistant, setup_integration: None, source: str +) -> None: + """Test config flow aborts when already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: source}, + data=CONF_DATA, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_cannot_connect( + hass: HomeAssistant, cannot_connect: None +) -> None: + """Test connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + data=CONF_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "cannot_connect" + + +async def test_flow_user_unknown_error(hass: HomeAssistant, unknown: None) -> None: + """Test unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "unknown" + + +async def test_flow_import(hass: HomeAssistant, connection: None) -> None: + """Test import step.""" + with _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=CONF_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "Netgear LM1200" + assert result["data"] == CONF_DATA + + +async def test_flow_import_failure(hass: HomeAssistant, cannot_connect: None) -> None: + """Test import step failure.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_IMPORT}, + data=CONF_DATA, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" diff --git a/tests/components/netgear_lte/test_init.py b/tests/components/netgear_lte/test_init.py new file mode 100644 index 0000000000000000000000000000000000000000..7c48d9d87d2187c5529269a6021c50ff2594ea59 --- /dev/null +++ b/tests/components/netgear_lte/test_init.py @@ -0,0 +1,28 @@ +"""Test Netgear LTE integration.""" +from homeassistant.components.netgear_lte.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from .conftest import CONF_DATA + + +async def test_setup_unload(hass: HomeAssistant, setup_integration: None) -> None: + """Test setup and unload.""" + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.state == ConfigEntryState.LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.data == CONF_DATA + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + + +async def test_async_setup_entry_not_ready( + hass: HomeAssistant, setup_cannot_connect: None +) -> None: + """Test that it throws ConfigEntryNotReady when exception occurs during setup.""" + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ConfigEntryState.SETUP_RETRY diff --git a/tests/components/netgear_lte/test_notify.py b/tests/components/netgear_lte/test_notify.py new file mode 100644 index 0000000000000000000000000000000000000000..12d906138c3e21e0b9a5de1a15dd34eab9f5734e --- /dev/null +++ b/tests/components/netgear_lte/test_notify.py @@ -0,0 +1,29 @@ +"""The tests for the Netgear LTE notify platform.""" +from unittest.mock import patch + +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_TARGET, + DOMAIN as NOTIFY_DOMAIN, +) +from homeassistant.core import HomeAssistant + +ICON_PATH = "/some/path" +MESSAGE = "one, two, testing, testing" + + +async def test_notify(hass: HomeAssistant, setup_integration: None) -> None: + """Test sending a message.""" + assert hass.services.has_service(NOTIFY_DOMAIN, "netgear_lm1200") + + with patch("homeassistant.components.netgear_lte.eternalegypt.Modem.sms") as mock: + await hass.services.async_call( + NOTIFY_DOMAIN, + "netgear_lm1200", + { + ATTR_MESSAGE: MESSAGE, + ATTR_TARGET: "5555555556", + }, + blocking=True, + ) + assert len(mock.mock_calls) == 1 diff --git a/tests/components/netgear_lte/test_sensor.py b/tests/components/netgear_lte/test_sensor.py new file mode 100644 index 0000000000000000000000000000000000000000..8682af9a5c37fa0791779c9bb4afde0110bd607b --- /dev/null +++ b/tests/components/netgear_lte/test_sensor.py @@ -0,0 +1,56 @@ +"""The tests for Netgear LTE sensor platform.""" +import pytest + +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfInformation, +) +from homeassistant.core import HomeAssistant + + +@pytest.mark.usefixtures("setup_integration", "entity_registry_enabled_by_default") +async def test_sensors(hass: HomeAssistant) -> None: + """Test for successfully setting up the Netgear LTE sensor platform.""" + state = hass.states.get("sensor.netgear_lte_cell_id") + assert state.state == "12345678" + state = hass.states.get("sensor.netgear_lte_connection_text") + assert state.state == "4G" + state = hass.states.get("sensor.netgear_lte_connection_type") + assert state.state == "IPv4AndIPv6" + state = hass.states.get("sensor.netgear_lte_current_band") + assert state.state == "LTE B4" + state = hass.states.get("sensor.netgear_lte_current_ps_service_type") + assert state.state == "LTE" + state = hass.states.get("sensor.netgear_lte_radio_quality") + assert state.state == "52" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + state = hass.states.get("sensor.netgear_lte_register_network_display") + assert state.state == "T-Mobile" + state = hass.states.get("sensor.netgear_lte_rx_level") + assert state.state == "-113" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) + state = hass.states.get("sensor.netgear_lte_sms") + assert state.state == "1" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "unread" + state = hass.states.get("sensor.netgear_lte_sms_total") + assert state.state == "1" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "messages" + state = hass.states.get("sensor.netgear_lte_tx_level") + assert state.state == "4" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) + state = hass.states.get("sensor.netgear_lte_upstream") + assert state.state == "LTE" + state = hass.states.get("sensor.netgear_lte_usage") + assert state.state == "40.5" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.MEBIBYTES + assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.DATA_SIZE diff --git a/tests/components/netgear_lte/test_services.py b/tests/components/netgear_lte/test_services.py new file mode 100644 index 0000000000000000000000000000000000000000..5c5c33be980a69f7c97f16610b078d82a63b3b07 --- /dev/null +++ b/tests/components/netgear_lte/test_services.py @@ -0,0 +1,55 @@ +"""Services tests for the Netgear LTE integration.""" +from unittest.mock import patch + +from homeassistant.components.netgear_lte.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from .conftest import HOST + + +async def test_set_option(hass: HomeAssistant, setup_integration: None) -> None: + """Test service call set option.""" + with patch( + "homeassistant.components.netgear_lte.eternalegypt.Modem.set_failover_mode" + ) as mock_client: + await hass.services.async_call( + DOMAIN, + "set_option", + {CONF_HOST: HOST, "failover": "auto", "autoconnect": "home"}, + blocking=True, + ) + assert len(mock_client.mock_calls) == 1 + + with patch( + "homeassistant.components.netgear_lte.eternalegypt.Modem.connect_lte" + ) as mock_client: + await hass.services.async_call( + DOMAIN, + "connect_lte", + {CONF_HOST: HOST}, + blocking=True, + ) + assert len(mock_client.mock_calls) == 1 + + with patch( + "homeassistant.components.netgear_lte.eternalegypt.Modem.disconnect_lte" + ) as mock_client: + await hass.services.async_call( + DOMAIN, + "disconnect_lte", + {CONF_HOST: HOST}, + blocking=True, + ) + assert len(mock_client.mock_calls) == 1 + + with patch( + "homeassistant.components.netgear_lte.eternalegypt.Modem.delete_sms" + ) as mock_client: + await hass.services.async_call( + DOMAIN, + "delete_sms", + {CONF_HOST: HOST, "sms_id": 1}, + blocking=True, + ) + assert len(mock_client.mock_calls) == 1