Add update coordinator to Netgear LTE (#115474)

This commit is contained in:
Robert Hillis 2024-06-18 03:12:02 -04:00 committed by GitHub
parent 2555827030
commit d5d906e148
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 166 additions and 194 deletions

View File

@ -1,25 +1,17 @@
"""Support for Netgear LTE modems.""" """Support for Netgear LTE modems."""
from datetime import timedelta from typing import Any
from aiohttp.cookiejar import CookieJar from aiohttp.cookiejar import CookieJar
import attr
import eternalegypt import eternalegypt
from eternalegypt.eternalegypt import SMS
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import ( from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, Platform
CONF_HOST, from homeassistant.core import HomeAssistant
CONF_NAME,
CONF_PASSWORD,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.aiohttp_client import async_create_clientsession 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.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
@ -28,14 +20,12 @@ from .const import (
ATTR_MESSAGE, ATTR_MESSAGE,
ATTR_SMS_ID, ATTR_SMS_ID,
DATA_HASS_CONFIG, DATA_HASS_CONFIG,
DISPATCHER_NETGEAR_LTE, DATA_SESSION,
DOMAIN, DOMAIN,
LOGGER,
) )
from .coordinator import NetgearLTEDataUpdateCoordinator
from .services import async_setup_services from .services import async_setup_services
SCAN_INTERVAL = timedelta(seconds=10)
EVENT_SMS = "netgear_lte_sms" EVENT_SMS = "netgear_lte_sms"
ALL_SENSORS = [ ALL_SENSORS = [
@ -65,54 +55,11 @@ PLATFORMS = [
Platform.NOTIFY, Platform.NOTIFY,
Platform.SENSOR, Platform.SENSOR,
] ]
type NetgearLTEConfigEntry = ConfigEntry[NetgearLTEDataUpdateCoordinator]
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
@attr.s
class ModemData:
"""Class for modem state."""
hass = attr.ib()
host = attr.ib()
modem = attr.ib()
data = attr.ib(init=False, default=None)
connected = attr.ib(init=False, default=True)
async def async_update(self):
"""Call the API to update the data."""
try:
self.data = await self.modem.information()
if not self.connected:
LOGGER.warning("Connected to %s", self.host)
self.connected = True
except eternalegypt.Error:
if self.connected:
LOGGER.warning("Lost connection to %s", self.host)
self.connected = False
self.data = None
async_dispatcher_send(self.hass, DISPATCHER_NETGEAR_LTE)
@attr.s
class LTEData:
"""Shared state."""
websession = attr.ib()
modem_data: dict[str, ModemData] = attr.ib(init=False, factory=dict)
def get_modem_data(self, config):
"""Get modem_data for the host in config."""
if config[CONF_HOST] is not None:
return self.modem_data.get(config[CONF_HOST])
if len(self.modem_data) != 1:
return None
return next(iter(self.modem_data.values()))
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Netgear LTE component.""" """Set up Netgear LTE component."""
hass.data[DATA_HASS_CONFIG] = config hass.data[DATA_HASS_CONFIG] = config
@ -120,44 +67,44 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: NetgearLTEConfigEntry) -> bool:
"""Set up Netgear LTE from a config entry.""" """Set up Netgear LTE from a config entry."""
host = entry.data[CONF_HOST] host = entry.data[CONF_HOST]
password = entry.data[CONF_PASSWORD] password = entry.data[CONF_PASSWORD]
if not (data := hass.data.get(DOMAIN)) or data.websession.closed: data: dict[str, Any] = hass.data.setdefault(DOMAIN, {})
websession = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) if not (session := data.get(DATA_SESSION)) or session.closed:
session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True))
modem = eternalegypt.Modem(hostname=host, websession=session)
hass.data[DOMAIN] = LTEData(websession) try:
await modem.login(password=password)
except eternalegypt.Error as ex:
raise ConfigEntryNotReady("Cannot connect/authenticate") from ex
modem = eternalegypt.Modem(hostname=host, websession=hass.data[DOMAIN].websession) def fire_sms_event(sms: SMS) -> None:
modem_data = ModemData(hass, host, modem) """Send an SMS event."""
data = {
ATTR_HOST: modem.hostname,
ATTR_SMS_ID: sms.id,
ATTR_FROM: sms.sender,
ATTR_MESSAGE: sms.message,
}
hass.bus.async_fire(EVENT_SMS, data)
await _login(hass, modem_data, password) await modem.add_sms_listener(fire_sms_event)
async def _update(now): coordinator = NetgearLTEDataUpdateCoordinator(hass, modem)
"""Periodic update.""" await coordinator.async_config_entry_first_refresh()
await modem_data.async_update() entry.runtime_data = coordinator
update_unsub = async_track_time_interval(hass, _update, SCAN_INTERVAL) await async_setup_services(hass, modem)
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))
await async_setup_services(hass)
await discovery.async_load_platform( await discovery.async_load_platform(
hass, hass,
Platform.NOTIFY, Platform.NOTIFY,
DOMAIN, DOMAIN,
{CONF_HOST: entry.data[CONF_HOST], CONF_NAME: entry.title}, {CONF_NAME: entry.title, "modem": modem},
hass.data[DATA_HASS_CONFIG], hass.data[DATA_HASS_CONFIG],
) )
@ -168,7 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: NetgearLTEConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
loaded_entries = [ loaded_entries = [
@ -178,28 +125,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
] ]
if len(loaded_entries) == 1: if len(loaded_entries) == 1:
hass.data.pop(DOMAIN, None) hass.data.pop(DOMAIN, None)
for service_name in hass.services.async_services()[DOMAIN]:
hass.services.async_remove(DOMAIN, service_name)
return unload_ok return unload_ok
async def _login(hass: HomeAssistant, modem_data: ModemData, password: str) -> None:
"""Log in and complete setup."""
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."""
data = {
ATTR_HOST: modem_data.host,
ATTR_SMS_ID: sms.id,
ATTR_FROM: sms.sender,
ATTR_MESSAGE: sms.message,
}
hass.bus.async_fire(EVENT_SMS, data)
await modem_data.modem.add_sms_listener(fire_sms_event)
await modem_data.async_update()
hass.data[DOMAIN].modem_data[modem_data.host] = modem_data

View File

@ -7,12 +7,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN from . import NetgearLTEConfigEntry
from .entity import LTEEntity from .entity import LTEEntity
BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = ( BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
@ -38,13 +37,13 @@ BINARY_SENSORS: tuple[BinarySensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: NetgearLTEConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Netgear LTE binary sensor.""" """Set up the Netgear LTE binary sensor."""
modem_data = hass.data[DOMAIN].get_modem_data(entry.data)
async_add_entities( async_add_entities(
NetgearLTEBinarySensor(entry, modem_data, sensor) for sensor in BINARY_SENSORS NetgearLTEBinarySensor(entry, description) for description in BINARY_SENSORS
) )
@ -54,4 +53,4 @@ class NetgearLTEBinarySensor(LTEEntity, BinarySensorEntity):
@property @property
def is_on(self): def is_on(self):
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
return getattr(self.modem_data.data, self.entity_description.key) return getattr(self.coordinator.data, self.entity_description.key)

View File

@ -16,9 +16,9 @@ CONF_NOTIFY: Final = "notify"
CONF_SENSOR: Final = "sensor" CONF_SENSOR: Final = "sensor"
DATA_HASS_CONFIG = "netgear_lte_hass_config" DATA_HASS_CONFIG = "netgear_lte_hass_config"
DATA_SESSION = "session"
# https://kb.netgear.com/31160/How-do-I-change-my-4G-LTE-Modem-s-IP-address-range # https://kb.netgear.com/31160/How-do-I-change-my-4G-LTE-Modem-s-IP-address-range
DEFAULT_HOST = "192.168.5.1" DEFAULT_HOST = "192.168.5.1"
DISPATCHER_NETGEAR_LTE = "netgear_lte_update"
DOMAIN: Final = "netgear_lte" DOMAIN: Final = "netgear_lte"
FAILOVER_MODES = ["auto", "wire", "mobile"] FAILOVER_MODES = ["auto", "wire", "mobile"]

View File

@ -0,0 +1,43 @@
"""Data update coordinator for the Netgear LTE integration."""
from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING
from eternalegypt.eternalegypt import Error, Information, Modem
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
if TYPE_CHECKING:
from . import NetgearLTEConfigEntry
class NetgearLTEDataUpdateCoordinator(DataUpdateCoordinator[Information]):
"""Data update coordinator for the Netgear LTE integration."""
config_entry: NetgearLTEConfigEntry
def __init__(
self,
hass: HomeAssistant,
modem: Modem,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass=hass,
logger=LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=10),
)
self.modem = modem
async def _async_update_data(self) -> Information:
"""Get the latest data."""
try:
return await self.modem.information()
except Error as ex:
raise UpdateFailed(ex) from ex

View File

@ -1,54 +1,36 @@
"""Entity representing a Netgear LTE entity.""" """Entity representing a Netgear LTE entity."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST from homeassistant.const import CONF_HOST
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import ModemData from . import NetgearLTEConfigEntry
from .const import DISPATCHER_NETGEAR_LTE, DOMAIN, MANUFACTURER from .const import DOMAIN, MANUFACTURER
from .coordinator import NetgearLTEDataUpdateCoordinator
class LTEEntity(Entity): class LTEEntity(CoordinatorEntity[NetgearLTEDataUpdateCoordinator]):
"""Base LTE entity.""" """Base LTE entity."""
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_should_poll = False
def __init__( def __init__(
self, self,
config_entry: ConfigEntry, entry: NetgearLTEConfigEntry,
modem_data: ModemData,
description: EntityDescription, description: EntityDescription,
) -> None: ) -> None:
"""Initialize a Netgear LTE entity.""" """Initialize a Netgear LTE entity."""
super().__init__(entry.runtime_data)
self.entity_description = description self.entity_description = description
self.modem_data = modem_data data = entry.runtime_data.data
self._attr_unique_id = f"{description.key}_{modem_data.data.serial_number}" self._attr_unique_id = f"{description.key}_{data.serial_number}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
configuration_url=f"http://{config_entry.data[CONF_HOST]}", configuration_url=f"http://{entry.data[CONF_HOST]}",
identifiers={(DOMAIN, modem_data.data.serial_number)}, identifiers={(DOMAIN, data.serial_number)},
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,
model=modem_data.data.items["general.model"], model=data.items["general.model"],
serial_number=modem_data.data.serial_number, serial_number=data.serial_number,
sw_version=modem_data.data.items["general.fwversion"], sw_version=data.items["general.fwversion"],
hw_version=modem_data.data.items["general.hwversion"], hw_version=data.items["general.hwversion"],
) )
async def async_added_to_hass(self) -> None:
"""Register callback."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, DISPATCHER_NETGEAR_LTE, self.async_write_ha_state
)
)
async def async_update(self) -> None:
"""Force update of state."""
await self.modem_data.async_update()
@property
def available(self) -> bool:
"""Return the availability of the sensor."""
return self.modem_data.data is not None

View File

@ -2,15 +2,17 @@
from __future__ import annotations from __future__ import annotations
import attr from typing import Any
import eternalegypt import eternalegypt
from eternalegypt.eternalegypt import Modem
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
from homeassistant.const import CONF_RECIPIENT from homeassistant.const import CONF_RECIPIENT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_NOTIFY, DOMAIN, LOGGER from .const import CONF_NOTIFY, LOGGER
async def async_get_service( async def async_get_service(
@ -22,21 +24,25 @@ async def async_get_service(
if discovery_info is None: if discovery_info is None:
return None return None
return NetgearNotifyService(hass, discovery_info) return NetgearNotifyService(config, discovery_info)
@attr.s
class NetgearNotifyService(BaseNotificationService): class NetgearNotifyService(BaseNotificationService):
"""Implementation of a notification service.""" """Implementation of a notification service."""
hass = attr.ib() def __init__(
config = attr.ib() self,
config: ConfigType,
discovery_info: dict[str, Any],
) -> None:
"""Initialize the service."""
self.config = config
self.modem: Modem = discovery_info["modem"]
async def async_send_message(self, message="", **kwargs): async def async_send_message(self, message="", **kwargs):
"""Send a message to a user.""" """Send a message to a user."""
modem_data = self.hass.data[DOMAIN].get_modem_data(self.config) if not self.modem.token:
if not modem_data:
LOGGER.error("Modem not ready") LOGGER.error("Modem not ready")
return return
if not (targets := kwargs.get(ATTR_TARGET)): if not (targets := kwargs.get(ATTR_TARGET)):
@ -50,6 +56,6 @@ class NetgearNotifyService(BaseNotificationService):
for target in targets: for target in targets:
try: try:
await modem_data.modem.sms(target, message) await self.modem.sms(target, message)
except eternalegypt.Error: except eternalegypt.Error:
LOGGER.error("Unable to send to %s", target) LOGGER.error("Unable to send to %s", target)

View File

@ -5,12 +5,13 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from eternalegypt.eternalegypt import Information
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
@ -21,8 +22,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from . import ModemData from . import NetgearLTEConfigEntry
from .const import DOMAIN
from .entity import LTEEntity from .entity import LTEEntity
@ -30,7 +30,7 @@ from .entity import LTEEntity
class NetgearLTESensorEntityDescription(SensorEntityDescription): class NetgearLTESensorEntityDescription(SensorEntityDescription):
"""Class describing Netgear LTE entities.""" """Class describing Netgear LTE entities."""
value_fn: Callable[[ModemData], StateType] | None = None value_fn: Callable[[Information], StateType] | None = None
SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = ( SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = (
@ -38,13 +38,13 @@ SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = (
key="sms", key="sms",
translation_key="sms", translation_key="sms",
native_unit_of_measurement="unread", native_unit_of_measurement="unread",
value_fn=lambda modem_data: sum(1 for x in modem_data.data.sms if x.unread), value_fn=lambda data: sum(1 for x in data.sms if x.unread),
), ),
NetgearLTESensorEntityDescription( NetgearLTESensorEntityDescription(
key="sms_total", key="sms_total",
translation_key="sms_total", translation_key="sms_total",
native_unit_of_measurement="messages", native_unit_of_measurement="messages",
value_fn=lambda modem_data: len(modem_data.data.sms), value_fn=lambda data: len(data.sms),
), ),
NetgearLTESensorEntityDescription( NetgearLTESensorEntityDescription(
key="usage", key="usage",
@ -54,7 +54,7 @@ SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfInformation.BYTES, native_unit_of_measurement=UnitOfInformation.BYTES,
suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES, suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES,
suggested_display_precision=1, suggested_display_precision=1,
value_fn=lambda modem_data: modem_data.data.usage, value_fn=lambda data: data.usage,
), ),
NetgearLTESensorEntityDescription( NetgearLTESensorEntityDescription(
key="radio_quality", key="radio_quality",
@ -125,14 +125,12 @@ SENSORS: tuple[NetgearLTESensorEntityDescription, ...] = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback hass: HomeAssistant,
entry: NetgearLTEConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Netgear LTE sensor.""" """Set up the Netgear LTE sensor."""
modem_data = hass.data[DOMAIN].get_modem_data(entry.data) async_add_entities(NetgearLTESensor(entry, description) for description in SENSORS)
async_add_entities(
NetgearLTESensor(entry, modem_data, sensor) for sensor in SENSORS
)
class NetgearLTESensor(LTEEntity, SensorEntity): class NetgearLTESensor(LTEEntity, SensorEntity):
@ -144,5 +142,5 @@ class NetgearLTESensor(LTEEntity, SensorEntity):
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self.entity_description.value_fn is not None: if self.entity_description.value_fn is not None:
return self.entity_description.value_fn(self.modem_data) return self.entity_description.value_fn(self.coordinator.data)
return getattr(self.modem_data.data, self.entity_description.key) return getattr(self.coordinator.data, self.entity_description.key)

View File

@ -1,10 +1,8 @@
"""Services for the Netgear LTE integration.""" """Services for the Netgear LTE integration."""
from typing import TYPE_CHECKING from eternalegypt.eternalegypt import Modem
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
@ -19,9 +17,6 @@ from .const import (
LOGGER, LOGGER,
) )
if TYPE_CHECKING:
from . import LTEData, ModemData
SERVICE_DELETE_SMS = "delete_sms" SERVICE_DELETE_SMS = "delete_sms"
SERVICE_SET_OPTION = "set_option" SERVICE_SET_OPTION = "set_option"
SERVICE_CONNECT_LTE = "connect_lte" SERVICE_CONNECT_LTE = "connect_lte"
@ -50,31 +45,29 @@ CONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string})
DISCONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string}) DISCONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string})
async def async_setup_services(hass: HomeAssistant) -> None: async def async_setup_services(hass: HomeAssistant, modem: Modem) -> None:
"""Set up services for Netgear LTE integration.""" """Set up services for Netgear LTE integration."""
async def service_handler(call: ServiceCall) -> None: async def service_handler(call: ServiceCall) -> None:
"""Apply a service.""" """Apply a service."""
host = call.data.get(ATTR_HOST) host = call.data.get(ATTR_HOST)
data: LTEData = hass.data[DOMAIN]
modem_data: ModemData = data.get_modem_data({CONF_HOST: host})
if not modem_data: if not modem.token:
LOGGER.error("%s: host %s unavailable", call.service, host) LOGGER.error("%s: host %s unavailable", call.service, host)
return return
if call.service == SERVICE_DELETE_SMS: if call.service == SERVICE_DELETE_SMS:
for sms_id in call.data[ATTR_SMS_ID]: for sms_id in call.data[ATTR_SMS_ID]:
await modem_data.modem.delete_sms(sms_id) await modem.delete_sms(sms_id)
elif call.service == SERVICE_SET_OPTION: elif call.service == SERVICE_SET_OPTION:
if failover := call.data.get(ATTR_FAILOVER): if failover := call.data.get(ATTR_FAILOVER):
await modem_data.modem.set_failover_mode(failover) await modem.set_failover_mode(failover)
if autoconnect := call.data.get(ATTR_AUTOCONNECT): if autoconnect := call.data.get(ATTR_AUTOCONNECT):
await modem_data.modem.set_autoconnect_mode(autoconnect) await modem.set_autoconnect_mode(autoconnect)
elif call.service == SERVICE_CONNECT_LTE: elif call.service == SERVICE_CONNECT_LTE:
await modem_data.modem.connect_lte() await modem.connect_lte()
elif call.service == SERVICE_DISCONNECT_LTE: elif call.service == SERVICE_DISCONNECT_LTE:
await modem_data.modem.disconnect_lte() await modem.disconnect_lte()
service_schemas = { service_schemas = {
SERVICE_DELETE_SMS: DELETE_SMS_SCHEMA, SERVICE_DELETE_SMS: DELETE_SMS_SCHEMA,

View File

@ -2,7 +2,6 @@
from unittest.mock import patch from unittest.mock import patch
from homeassistant import data_entry_flow
from homeassistant.components.netgear_lte.const import DOMAIN from homeassistant.components.netgear_lte.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_SOURCE from homeassistant.const import CONF_SOURCE
@ -25,7 +24,7 @@ async def test_flow_user_form(hass: HomeAssistant, connection: None) -> None:
context={CONF_SOURCE: SOURCE_USER}, context={CONF_SOURCE: SOURCE_USER},
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
with _patch_setup(): with _patch_setup():
@ -33,7 +32,7 @@ async def test_flow_user_form(hass: HomeAssistant, connection: None) -> None:
result["flow_id"], result["flow_id"],
user_input=CONF_DATA, user_input=CONF_DATA,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Netgear LM1200" assert result["title"] == "Netgear LM1200"
assert result["data"] == CONF_DATA assert result["data"] == CONF_DATA
assert result["context"]["unique_id"] == "FFFFFFFFFFFFF" assert result["context"]["unique_id"] == "FFFFFFFFFFFFF"
@ -63,7 +62,7 @@ async def test_flow_user_cannot_connect(
data=CONF_DATA, data=CONF_DATA,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"]["base"] == "cannot_connect" assert result["errors"]["base"] == "cannot_connect"
@ -78,6 +77,6 @@ async def test_flow_user_unknown_error(hass: HomeAssistant, unknown: None) -> No
result["flow_id"], result["flow_id"],
user_input=CONF_DATA, user_input=CONF_DATA,
) )
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
assert result["errors"]["base"] == "unknown" assert result["errors"]["base"] == "unknown"

View File

@ -1,14 +1,22 @@
"""Test Netgear LTE integration.""" """Test Netgear LTE integration."""
from datetime import timedelta
from unittest.mock import patch
from eternalegypt.eternalegypt import Error
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.netgear_lte.const import DOMAIN from homeassistant.components.netgear_lte.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
import homeassistant.util.dt as dt_util
from .conftest import CONF_DATA from .conftest import CONF_DATA
from tests.common import async_fire_time_changed
async def test_setup_unload(hass: HomeAssistant, setup_integration: None) -> None: async def test_setup_unload(hass: HomeAssistant, setup_integration: None) -> None:
"""Test setup and unload.""" """Test setup and unload."""
@ -43,3 +51,21 @@ async def test_device(
await hass.async_block_till_done() await hass.async_block_till_done()
device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)}) device = device_registry.async_get_device(identifiers={(DOMAIN, entry.unique_id)})
assert device == snapshot assert device == snapshot
async def test_update_failed(
hass: HomeAssistant,
entity_registry_enabled_by_default: None,
setup_integration: None,
) -> None:
"""Test coordinator throws UpdateFailed after failed update."""
with patch(
"homeassistant.components.netgear_lte.eternalegypt.Modem.information",
side_effect=Error,
) as updater:
next_update = dt_util.utcnow() + timedelta(seconds=10)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
updater.assert_called_once()
state = hass.states.get("sensor.netgear_lm1200_radio_quality")
assert state.state == STATE_UNAVAILABLE