Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 77 additions & 4 deletions homeassistant/components/sms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
"""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
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 .const import CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED, DOMAIN, SMS_GATEWAY
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

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__)
Expand All @@ -30,6 +43,8 @@
extra=vol.ALLOW_EXTRA,
)

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Configure Gammu state machine."""
Expand Down Expand Up @@ -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
Expand All @@ -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
67 changes: 67 additions & 0 deletions homeassistant/components/sms/const.py
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -27,3 +36,61 @@
{"value": "76800", "label": "76800"},
{"value": "115200", "label": "115200"},
]

SIGNAL_SENSORS: Final[dict[str, SensorEntityDescription]] = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the sensor entity descriptions to the sensor platform. These are platform details.

"SignalStrength": SensorEntityDescription(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a future PR, you can add state_class=MEASUREMENT for this and others so stats are being generated

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]] = {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the dict keys are not used, why are these dicts and not tuples?

"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,
),
}
4 changes: 4 additions & 0 deletions homeassistant/components/sms/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ async def get_signal_quality_async(self):
"""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()
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/sms/notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from homeassistant.const import CONF_NAME, CONF_RECIPIENT
import homeassistant.helpers.config_validation as cv

from .const import DOMAIN, SMS_GATEWAY
from .const import DOMAIN, GATEWAY, SMS_GATEWAY

_LOGGER = logging.getLogger(__name__)

Expand All @@ -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]
Expand Down
101 changes: 42 additions & 59 deletions homeassistant/components/sms/sensor.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,67 @@
"""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(
hass: HomeAssistant,
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,
)
)
for description in NETWORK_SENSORS.values():
entities.append(
DeviceSensor(
network_coordinator,
description,
unique_id,
)
],
True,
)
)
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)