diff --git a/homeassistant/components/sms/__init__.py b/homeassistant/components/sms/__init__.py index b1c2703409c..0b63a3d0366 100644 --- a/homeassistant/components/sms/__init__.py +++ b/homeassistant/components/sms/__init__.py @@ -1,6 +1,9 @@ """The sms component.""" +from datetime import timedelta import logging +import async_timeout +import gammu # pylint: disable=import-error import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -8,8 +11,18 @@ from homeassistant.const import CONF_DEVICE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED, DOMAIN, SMS_GATEWAY +from .const import ( + CONF_BAUD_SPEED, + DEFAULT_BAUD_SPEED, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + GATEWAY, + NETWORK_COORDINATOR, + SIGNAL_COORDINATOR, + SMS_GATEWAY, +) from .gateway import create_sms_gateway _LOGGER = logging.getLogger(__name__) @@ -30,6 +43,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Configure Gammu state machine.""" @@ -61,7 +76,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: gateway = await create_sms_gateway(config, hass) if not gateway: return False - hass.data[DOMAIN][SMS_GATEWAY] = gateway + + signal_coordinator = SignalCoordinator(hass, gateway) + network_coordinator = NetworkCoordinator(hass, gateway) + + # Fetch initial data so we have data when entities subscribe + await signal_coordinator.async_config_entry_first_refresh() + await network_coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][SMS_GATEWAY] = { + SIGNAL_COORDINATOR: signal_coordinator, + NETWORK_COORDINATOR: network_coordinator, + GATEWAY: gateway, + } + hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -71,7 +100,51 @@ 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) if unload_ok: - gateway = hass.data[DOMAIN].pop(SMS_GATEWAY) + gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY] await gateway.terminate_async() return unload_ok + + +class SignalCoordinator(DataUpdateCoordinator): + """Signal strength coordinator.""" + + def __init__(self, hass, gateway): + """Initialize signal strength coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Device signal state", + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._gateway = gateway + + async def _async_update_data(self): + """Fetch device signal quality.""" + try: + async with async_timeout.timeout(10): + return await self._gateway.get_signal_quality_async() + except gammu.GSMError as exc: + raise UpdateFailed(f"Error communicating with device: {exc}") from exc + + +class NetworkCoordinator(DataUpdateCoordinator): + """Network info coordinator.""" + + def __init__(self, hass, gateway): + """Initialize network info coordinator.""" + super().__init__( + hass, + _LOGGER, + name="Device network state", + update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL), + ) + self._gateway = gateway + + async def _async_update_data(self): + """Fetch device network info.""" + try: + async with async_timeout.timeout(10): + return await self._gateway.get_network_info_async() + except gammu.GSMError as exc: + raise UpdateFailed(f"Error communicating with device: {exc}") from exc diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py index 7c40a04073c..858e53d9808 100644 --- a/homeassistant/components/sms/const.py +++ b/homeassistant/components/sms/const.py @@ -1,8 +1,17 @@ """Constants for sms Component.""" +from typing import Final + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription +from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS +from homeassistant.helpers.entity import EntityCategory DOMAIN = "sms" SMS_GATEWAY = "SMS_GATEWAY" SMS_STATE_UNREAD = "UnRead" +SIGNAL_COORDINATOR = "signal_coordinator" +NETWORK_COORDINATOR = "network_coordinator" +GATEWAY = "gateway" +DEFAULT_SCAN_INTERVAL = 30 CONF_BAUD_SPEED = "baud_speed" DEFAULT_BAUD_SPEED = "0" DEFAULT_BAUD_SPEEDS = [ @@ -27,3 +36,61 @@ DEFAULT_BAUD_SPEEDS = [ {"value": "76800", "label": "76800"}, {"value": "115200", "label": "115200"}, ] + +SIGNAL_SENSORS: Final[dict[str, SensorEntityDescription]] = { + "SignalStrength": SensorEntityDescription( + key="SignalStrength", + name="Signal Strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + entity_registry_enabled_default=False, + ), + "SignalPercent": SensorEntityDescription( + key="SignalPercent", + icon="mdi:signal-cellular-3", + name="Signal Percent", + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=True, + ), + "BitErrorRate": SensorEntityDescription( + key="BitErrorRate", + name="Bit Error Rate", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + entity_registry_enabled_default=False, + ), +} + +NETWORK_SENSORS: Final[dict[str, SensorEntityDescription]] = { + "NetworkName": SensorEntityDescription( + key="NetworkName", + name="Network Name", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "State": SensorEntityDescription( + key="State", + name="Network Status", + entity_registry_enabled_default=True, + ), + "NetworkCode": SensorEntityDescription( + key="NetworkCode", + name="GSM network code", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "CID": SensorEntityDescription( + key="CID", + name="Cell ID", + icon="mdi:radio-tower", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "LAC": SensorEntityDescription( + key="LAC", + name="Local Area Code", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), +} diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 09992600943..c469e688737 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -154,6 +154,10 @@ class Gateway: """Get the current signal level of the modem.""" return await self._worker.get_signal_quality_async() + async def get_network_info_async(self): + """Get the current network info of the modem.""" + return await self._worker.get_network_info_async() + async def terminate_async(self): """Terminate modem connection.""" return await self._worker.terminate_async() diff --git a/homeassistant/components/sms/notify.py b/homeassistant/components/sms/notify.py index 433144773f7..d076f3625ba 100644 --- a/homeassistant/components/sms/notify.py +++ b/homeassistant/components/sms/notify.py @@ -8,7 +8,7 @@ from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationSer from homeassistant.const import CONF_NAME, CONF_RECIPIENT, CONF_TARGET import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, SMS_GATEWAY +from .const import DOMAIN, GATEWAY, SMS_GATEWAY _LOGGER = logging.getLogger(__name__) @@ -24,7 +24,7 @@ def get_service(hass, config, discovery_info=None): _LOGGER.error("SMS gateway not found, cannot initialize service") return - gateway = hass.data[DOMAIN][SMS_GATEWAY] + gateway = hass.data[DOMAIN][SMS_GATEWAY][GATEWAY] if discovery_info is None: number = config[CONF_RECIPIENT] diff --git a/homeassistant/components/sms/sensor.py b/homeassistant/components/sms/sensor.py index dcc85c4f8c6..de20a5b5d0f 100644 --- a/homeassistant/components/sms/sensor.py +++ b/homeassistant/components/sms/sensor.py @@ -1,22 +1,20 @@ """Support for SMS dongle sensor.""" -import logging - -import gammu # pylint: disable=import-error - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, SMS_GATEWAY - -_LOGGER = logging.getLogger(__name__) +from .const import ( + DOMAIN, + GATEWAY, + NETWORK_COORDINATOR, + NETWORK_SENSORS, + SIGNAL_COORDINATOR, + SIGNAL_SENSORS, + SMS_GATEWAY, +) async def async_setup_entry( @@ -24,61 +22,46 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up the GSM Signal Sensor sensor.""" - gateway = hass.data[DOMAIN][SMS_GATEWAY] - imei = await gateway.get_imei_async() - async_add_entities( - [ - GSMSignalSensor( - hass, - gateway, - imei, - SensorEntityDescription( - key="signal", - name=f"gsm_signal_imei_{imei}", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, - native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, - entity_registry_enabled_default=False, - ), + """Set up all device sensors.""" + sms_data = hass.data[DOMAIN][SMS_GATEWAY] + signal_coordinator = sms_data[SIGNAL_COORDINATOR] + network_coordinator = sms_data[NETWORK_COORDINATOR] + gateway = sms_data[GATEWAY] + unique_id = str(await gateway.get_imei_async()) + entities = [] + for description in SIGNAL_SENSORS.values(): + entities.append( + DeviceSensor( + signal_coordinator, + description, + unique_id, ) - ], - True, - ) + ) + for description in NETWORK_SENSORS.values(): + entities.append( + DeviceSensor( + network_coordinator, + description, + unique_id, + ) + ) + async_add_entities(entities, True) -class GSMSignalSensor(SensorEntity): - """Implementation of a GSM Signal sensor.""" +class DeviceSensor(CoordinatorEntity, SensorEntity): + """Implementation of a device sensor.""" - def __init__(self, hass, gateway, imei, description): - """Initialize the GSM Signal sensor.""" + def __init__(self, coordinator, description, unique_id): + """Initialize the device sensor.""" + super().__init__(coordinator) self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, str(imei))}, + identifiers={(DOMAIN, unique_id)}, name="SMS Gateway", ) - self._attr_unique_id = str(imei) - self._hass = hass - self._gateway = gateway - self._state = None + self._attr_unique_id = f"{unique_id}_{description.key}" self.entity_description = description - @property - def available(self): - """Return if the sensor data are available.""" - return self._state is not None - @property def native_value(self): """Return the state of the device.""" - return self._state["SignalStrength"] - - async def async_update(self): - """Get the latest data from the modem.""" - try: - self._state = await self._gateway.get_signal_quality_async() - except gammu.GSMError as exc: - _LOGGER.error("Failed to read signal quality: %s", exc) - - @property - def extra_state_attributes(self): - """Return the sensor attributes.""" - return self._state + return self.coordinator.data.get(self.entity_description.key)