1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Remove sms integration (#155460)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Robert Resch
2025-10-29 23:21:02 +01:00
committed by GitHub
parent 5c96b11479
commit 6088f5eef5
23 changed files with 3 additions and 1071 deletions

View File

@@ -502,7 +502,6 @@ jobs:
libavfilter-dev \
libavformat-dev \
libavutil-dev \
libgammu-dev \
libswresample-dev \
libswscale-dev \
libudev-dev
@@ -801,8 +800,7 @@ jobs:
-o Dir::State::Lists=${{ env.APT_LIST_CACHE_DIR }} \
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev
libturbojpeg
- *checkout
- *setup-python-default
- *cache-restore-python-default
@@ -853,7 +851,6 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev \
libxml2-utils
- *checkout
- *setup-python-matrix
@@ -1233,7 +1230,6 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev \
libxml2-utils
- *checkout
- *setup-python-matrix

View File

@@ -228,7 +228,7 @@ jobs:
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"

2
CODEOWNERS generated
View File

@@ -1479,8 +1479,6 @@ build.json @home-assistant/supervisor
/tests/components/smhi/ @gjohansson-ST
/homeassistant/components/smlight/ @tl-sl
/tests/components/smlight/ @tl-sl
/homeassistant/components/sms/ @ocalvo
/tests/components/sms/ @ocalvo
/homeassistant/components/snapcast/ @luar123
/tests/components/snapcast/ @luar123
/homeassistant/components/snmp/ @nmaggioni

View File

@@ -13,7 +13,6 @@ RUN \
libavcodec-dev \
libavdevice-dev \
libavutil-dev \
libgammu-dev \
libswscale-dev \
libswresample-dev \
libavfilter-dev \

View File

@@ -1,126 +0,0 @@
"""The sms component."""
import logging
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_BAUD_SPEED,
DEFAULT_BAUD_SPEED,
DOMAIN,
GATEWAY,
HASS_CONFIG,
NETWORK_COORDINATOR,
SIGNAL_COORDINATOR,
SMS_GATEWAY,
)
from .coordinator import NetworkCoordinator, SignalCoordinator
from .gateway import create_sms_gateway
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
SMS_CONFIG_SCHEMA = {vol.Required(CONF_DEVICE): cv.isdevice}
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
vol.All(
cv.deprecated(CONF_DEVICE),
SMS_CONFIG_SCHEMA,
),
)
},
extra=vol.ALLOW_EXTRA,
)
DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Configure Gammu state machine."""
hass.data.setdefault(DOMAIN, {})
hass.data[HASS_CONFIG] = config
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Configure Gammu state machine."""
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
DEPRECATED_ISSUE_ID,
breaks_in_ha_version="2025.12.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_system_packages_config_flow_integration",
translation_placeholders={
"integration_title": "SMS notifications via GSM-modem",
},
)
device = entry.data[CONF_DEVICE]
connection_mode = "at"
baud_speed = entry.data.get(CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED)
if baud_speed != DEFAULT_BAUD_SPEED:
connection_mode += baud_speed
config = {"Device": device, "Connection": connection_mode}
_LOGGER.debug("Connecting mode:%s", connection_mode)
gateway = await create_sms_gateway(config, hass)
if not gateway:
raise ConfigEntryNotReady(f"Cannot find device {device}")
signal_coordinator = SignalCoordinator(hass, entry, gateway)
network_coordinator = NetworkCoordinator(hass, entry, 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,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# set up notify platform, no entry support for notify component yet,
# have to use discovery to load platform.
hass.async_create_task(
discovery.async_load_platform(
hass,
Platform.NOTIFY,
DOMAIN,
{CONF_NAME: DOMAIN},
hass.data[HASS_CONFIG],
)
)
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)
if unload_ok:
gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY]
await gateway.terminate_async()
if not hass.config_entries.async_loaded_entries(DOMAIN):
async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID)
return unload_ok

View File

@@ -1,87 +0,0 @@
"""Config flow for SMS integration."""
import logging
from typing import Any
import gammu
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_DEVICE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector
from .const import CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED, DEFAULT_BAUD_SPEEDS, DOMAIN
from .gateway import create_sms_gateway
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE): str,
vol.Optional(CONF_BAUD_SPEED, default=DEFAULT_BAUD_SPEED): selector.selector(
{"select": {"options": DEFAULT_BAUD_SPEEDS}}
),
}
)
async def get_imei_from_config(hass: HomeAssistant, data: dict[str, Any]) -> str:
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
device = data[CONF_DEVICE]
connection_mode = "at"
baud_speed = data.get(CONF_BAUD_SPEED, DEFAULT_BAUD_SPEED)
if baud_speed != DEFAULT_BAUD_SPEED:
connection_mode += baud_speed
config = {"Device": device, "Connection": connection_mode}
gateway = await create_sms_gateway(config, hass)
if not gateway:
raise CannotConnect
try:
imei = await gateway.get_imei_async()
except gammu.GSMError as err:
raise CannotConnect from err
finally:
await gateway.terminate_async()
# Return info that you want to store in the config entry.
return imei
class SMSFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for SMS integration."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
errors = {}
if user_input is not None:
try:
imei = await get_imei_from_config(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
await self.async_set_unique_id(imei)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=imei, data=user_input)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@@ -1,35 +0,0 @@
"""Constants for sms Component."""
DOMAIN = "sms"
SMS_GATEWAY = "SMS_GATEWAY"
HASS_CONFIG = "sms_hass_config"
SMS_STATE_UNREAD = "UnRead"
SIGNAL_COORDINATOR = "signal_coordinator"
NETWORK_COORDINATOR = "network_coordinator"
GATEWAY = "gateway"
DEFAULT_SCAN_INTERVAL = 30
CONF_BAUD_SPEED = "baud_speed"
CONF_UNICODE = "unicode"
DEFAULT_BAUD_SPEED = "0"
DEFAULT_BAUD_SPEEDS = [
{"value": DEFAULT_BAUD_SPEED, "label": "Auto"},
{"value": "50", "label": "50"},
{"value": "75", "label": "75"},
{"value": "110", "label": "110"},
{"value": "134", "label": "134"},
{"value": "150", "label": "150"},
{"value": "200", "label": "200"},
{"value": "300", "label": "300"},
{"value": "600", "label": "600"},
{"value": "1200", "label": "1200"},
{"value": "1800", "label": "1800"},
{"value": "2400", "label": "2400"},
{"value": "4800", "label": "4800"},
{"value": "9600", "label": "9600"},
{"value": "19200", "label": "19200"},
{"value": "28800", "label": "28800"},
{"value": "38400", "label": "38400"},
{"value": "57600", "label": "57600"},
{"value": "76800", "label": "76800"},
{"value": "115200", "label": "115200"},
]

View File

@@ -1,59 +0,0 @@
"""DataUpdateCoordinators for the sms integration."""
import asyncio
from datetime import timedelta
import logging
import gammu
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
class SignalCoordinator(DataUpdateCoordinator):
"""Signal strength coordinator."""
def __init__(self, hass, entry, gateway):
"""Initialize signal strength coordinator."""
super().__init__(
hass,
_LOGGER,
name="Device signal state",
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
config_entry=entry,
)
self._gateway = gateway
async def _async_update_data(self):
"""Fetch device signal quality."""
try:
async with asyncio.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, entry, gateway):
"""Initialize network info coordinator."""
super().__init__(
hass,
_LOGGER,
name="Device network state",
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
config_entry=entry,
)
self._gateway = gateway
async def _async_update_data(self):
"""Fetch device network info."""
try:
async with asyncio.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

View File

@@ -1,211 +0,0 @@
"""The sms gateway to interact with a GSM modem."""
import logging
import gammu
from gammu.asyncworker import GammuAsyncWorker
from homeassistant.core import callback
from .const import DOMAIN, SMS_STATE_UNREAD
_LOGGER = logging.getLogger(__name__)
class Gateway:
"""SMS gateway to interact with a GSM modem."""
def __init__(self, config, hass):
"""Initialize the sms gateway."""
_LOGGER.debug("Init with connection mode:%s", config["Connection"])
self._worker = GammuAsyncWorker(self.sms_pull)
self._worker.configure(config)
self._hass = hass
self._first_pull = True
self.manufacturer = None
self.model = None
self.firmware = None
async def init_async(self):
"""Initialize the sms gateway asynchronously. This method is also called in config flow to verify connection."""
await self._worker.init_async()
self.manufacturer = await self.get_manufacturer_async()
self.model = await self.get_model_async()
self.firmware = await self.get_firmware_async()
def sms_pull(self, state_machine):
"""Pull device.
@param state_machine: state machine
@type state_machine: gammu.StateMachine
"""
state_machine.ReadDevice()
_LOGGER.debug("Pulling modem")
self.sms_read_messages(state_machine, self._first_pull)
self._first_pull = False
def sms_read_messages(self, state_machine, force=False):
"""Read all received SMS messages.
@param state_machine: state machine which invoked action
@type state_machine: gammu.StateMachine
"""
entries = self.get_and_delete_all_sms(state_machine, force)
_LOGGER.debug("SMS entries:%s", entries)
data = []
for entry in entries:
decoded_entry = gammu.DecodeSMS(entry)
message = entry[0]
_LOGGER.debug("Processing sms:%s,decoded:%s", message, decoded_entry)
sms_state = message["State"]
_LOGGER.debug("SMS state:%s", sms_state)
if sms_state == SMS_STATE_UNREAD:
if decoded_entry is None:
text = message["Text"]
else:
text = ""
for inner_entry in decoded_entry["Entries"]:
if inner_entry["Buffer"] is not None:
text += inner_entry["Buffer"]
event_data = {
"phone": message["Number"],
"date": str(message["DateTime"]),
"message": text,
}
_LOGGER.debug("Append event data:%s", event_data)
data.append(event_data)
self._hass.add_job(self._notify_incoming_sms, data)
def get_and_delete_all_sms(self, state_machine, force=False):
"""Read and delete all SMS in the modem."""
# Read SMS memory status ...
memory = state_machine.GetSMSStatus()
# ... and calculate number of messages
remaining = memory["SIMUsed"] + memory["PhoneUsed"]
start_remaining = remaining
# Get all sms
start = True
entries = []
all_parts = -1
_LOGGER.debug("Start remaining:%i", start_remaining)
try:
while remaining > 0:
if start:
entry = state_machine.GetNextSMS(Folder=0, Start=True)
all_parts = entry[0]["UDH"]["AllParts"]
part_number = entry[0]["UDH"]["PartNumber"]
part_is_missing = all_parts > start_remaining
_LOGGER.debug("All parts:%i", all_parts)
_LOGGER.debug("Part Number:%i", part_number)
_LOGGER.debug("Remaining:%i", remaining)
_LOGGER.debug("Start is_part_missing:%s", part_is_missing)
start = False
else:
entry = state_machine.GetNextSMS(
Folder=0, Location=entry[0]["Location"]
)
if part_is_missing and not force:
_LOGGER.debug("Not all parts have arrived")
break
remaining = remaining - 1
entries.append(entry)
# delete retrieved sms
_LOGGER.debug("Deleting message")
try:
state_machine.DeleteSMS(Folder=0, Location=entry[0]["Location"])
except gammu.ERR_MEMORY_NOT_AVAILABLE:
_LOGGER.error("Error deleting SMS, memory not available")
except gammu.ERR_EMPTY:
# error is raised if memory is empty (this induces wrong reported
# memory status)
_LOGGER.warning("Failed to read messages!")
# Link all SMS when there are concatenated messages
return gammu.LinkSMS(entries)
@callback
def _notify_incoming_sms(self, messages):
"""Notify hass when an incoming SMS message is received."""
for message in messages:
event_data = {
"phone": message["phone"],
"date": message["date"],
"text": message["message"],
}
self._hass.bus.async_fire(f"{DOMAIN}.incoming_sms", event_data)
async def send_sms_async(self, message):
"""Send sms message via the worker."""
return await self._worker.send_sms_async(message)
async def get_imei_async(self):
"""Get the IMEI of the device."""
return await self._worker.get_imei_async()
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."""
network_info = await self._worker.get_network_info_async()
# Looks like there is a bug and it's empty for any modem https://github.com/gammu/python-gammu/issues/31, so try workaround
if not network_info["NetworkName"]:
network_info["NetworkName"] = gammu.GSMNetworks.get(
network_info["NetworkCode"]
)
return network_info
async def get_manufacturer_async(self):
"""Get the manufacturer of the modem."""
return await self._worker.get_manufacturer_async()
async def get_model_async(self):
"""Get the model of the modem."""
model = await self._worker.get_model_async()
if not model or not model[0]:
return None
display = model[0] # Identification model
if model[1]: # Real model
display = f"{display} ({model[1]})"
return display
async def get_firmware_async(self):
"""Get the firmware information of the modem."""
firmware = await self._worker.get_firmware_async()
if not firmware or not firmware[0]:
return None
display = firmware[0] # Version
if firmware[1]: # Date
display = f"{display} ({firmware[1]})"
return display
async def terminate_async(self):
"""Terminate modem connection."""
return await self._worker.terminate_async()
async def create_sms_gateway(config, hass):
"""Create the sms gateway."""
try:
gateway = Gateway(config, hass)
try:
await gateway.init_async()
except gammu.GSMError as exc:
_LOGGER.error("Failed to initialize, error %s", exc)
await gateway.terminate_async()
return None
except gammu.GSMError as exc:
_LOGGER.error("Failed to create async worker, error %s", exc)
return None
return gateway

View File

@@ -1,12 +0,0 @@
{
"entity": {
"sensor": {
"cid": {
"default": "mdi:radio-tower"
},
"signal_percent": {
"default": "mdi:signal-cellular-3"
}
}
}
}

View File

@@ -1,10 +0,0 @@
{
"domain": "sms",
"name": "SMS notifications via GSM-modem",
"codeowners": ["@ocalvo"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/sms",
"iot_class": "local_polling",
"loggers": ["gammu"],
"requirements": ["python-gammu==3.2.4"]
}

View File

@@ -1,85 +0,0 @@
"""Support for SMS notification services."""
from __future__ import annotations
import logging
import gammu
from homeassistant.components.notify import ATTR_DATA, BaseNotificationService
from homeassistant.const import CONF_TARGET
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_UNICODE, DOMAIN, GATEWAY, SMS_GATEWAY
_LOGGER = logging.getLogger(__name__)
async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> SMSNotificationService | None:
"""Get the SMS notification service."""
if discovery_info is None:
return None
return SMSNotificationService(hass)
class SMSNotificationService(BaseNotificationService):
"""Implement the notification service for SMS."""
def __init__(self, hass):
"""Initialize the service."""
self.hass = hass
async def async_send_message(self, message="", **kwargs):
"""Send SMS message."""
if SMS_GATEWAY not in self.hass.data[DOMAIN]:
_LOGGER.error("SMS gateway not found, cannot send message")
return
gateway = self.hass.data[DOMAIN][SMS_GATEWAY][GATEWAY]
targets = kwargs.get(CONF_TARGET)
if targets is None:
_LOGGER.error("No target number specified, cannot send message")
return
extended_data = kwargs.get(ATTR_DATA)
_LOGGER.debug("Extended data:%s", extended_data)
if extended_data is None:
is_unicode = True
else:
is_unicode = extended_data.get(CONF_UNICODE, True)
smsinfo = {
"Class": -1,
"Unicode": is_unicode,
"Entries": [{"ID": "ConcatenatedTextLong", "Buffer": message}],
}
try:
# Encode messages
encoded = gammu.EncodeSMS(smsinfo)
except gammu.GSMError as exc:
_LOGGER.error("Encoding message %s failed: %s", message, exc)
return
# Send messages
for encoded_message in encoded:
# Fill in numbers
encoded_message["SMSC"] = {"Location": 1}
for target in targets:
encoded_message["Number"] = target
try:
# Actually send the message
await gateway.send_sms_async(encoded_message)
except gammu.GSMError as exc:
_LOGGER.error("Sending to %s failed: %s", target, exc)

View File

@@ -1,120 +0,0 @@
"""Support for SMS dongle sensor."""
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, GATEWAY, NETWORK_COORDINATOR, SIGNAL_COORDINATOR, SMS_GATEWAY
SIGNAL_SENSORS = (
SensorEntityDescription(
key="SignalStrength",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="SignalPercent",
translation_key="signal_percent",
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=True,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="BitErrorRate",
translation_key="bit_error_rate",
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
),
)
NETWORK_SENSORS = (
SensorEntityDescription(
key="NetworkName",
translation_key="network_name",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="State",
translation_key="state",
entity_registry_enabled_default=True,
),
SensorEntityDescription(
key="NetworkCode",
translation_key="network_code",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="CID",
translation_key="cid",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
SensorEntityDescription(
key="LAC",
translation_key="lac",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""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 = [
DeviceSensor(signal_coordinator, description, unique_id, gateway)
for description in SIGNAL_SENSORS
]
entities.extend(
DeviceSensor(network_coordinator, description, unique_id, gateway)
for description in NETWORK_SENSORS
)
async_add_entities(entities, True)
class DeviceSensor(CoordinatorEntity, SensorEntity):
"""Implementation of a device sensor."""
_attr_has_entity_name = True
def __init__(self, coordinator, description, unique_id, gateway):
"""Initialize the device sensor."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name="SMS Gateway",
manufacturer=gateway.manufacturer,
model=gateway.model,
sw_version=gateway.firmware,
)
self._attr_unique_id = f"{unique_id}_{description.key}"
self.entity_description = description
@property
def native_value(self):
"""Return the state of the device."""
return self.coordinator.data.get(self.entity_description.key)

View File

@@ -1,46 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"baud_speed": "Baud Speed",
"device": "[%key:common::config_flow::data::device%]"
},
"title": "Connect to the modem"
}
}
},
"entity": {
"sensor": {
"bit_error_rate": {
"name": "Bit error rate"
},
"cid": {
"name": "Cell ID"
},
"lac": {
"name": "Local area code"
},
"network_code": {
"name": "GSM network code"
},
"network_name": {
"name": "Network name"
},
"signal_percent": {
"name": "Signal percent"
},
"state": {
"name": "Network status"
}
}
}
}

View File

@@ -612,7 +612,6 @@ FLOWS = {
"smarty",
"smhi",
"smlight",
"sms",
"snapcast",
"snoo",
"snooz",

View File

@@ -6217,12 +6217,6 @@
"config_flow": true,
"iot_class": "local_push"
},
"sms": {
"name": "SMS notifications via GSM-modem",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
"smtp": {
"name": "SMTP",
"integration_type": "hub",

3
requirements_all.txt generated
View File

@@ -2477,9 +2477,6 @@ python-family-hub-local==0.0.2
# homeassistant.components.fully_kiosk
python-fullykiosk==0.0.14
# homeassistant.components.sms
# python-gammu==3.2.4
# homeassistant.components.gc100
python-gc100==1.0.3a0

View File

@@ -2062,9 +2062,6 @@ python-ecobee-api==0.2.20
# homeassistant.components.fully_kiosk
python-fullykiosk==0.0.14
# homeassistant.components.sms
# python-gammu==3.2.4
# homeassistant.components.google_drive
python-google-drive-api==0.1.0

View File

@@ -31,7 +31,6 @@ EXCLUDED_REQUIREMENTS_ALL = {
"face-recognition",
"pybluez",
"pycups",
"python-gammu",
"python-lirc",
"pyuserinput",
}
@@ -41,7 +40,6 @@ EXCLUDED_REQUIREMENTS_ALL = {
INCLUDED_REQUIREMENTS_WHEELS = {
"evdev",
"pycups",
"python-gammu",
"pyuserinput",
}
@@ -55,7 +53,7 @@ INCLUDED_REQUIREMENTS_WHEELS = {
OVERRIDDEN_REQUIREMENTS_ACTIONS = {
"pytest": {
"exclude": set(),
"include": {"python-gammu"},
"include": set(),
"markers": {},
},
"wheels_aarch64": {

View File

@@ -1 +0,0 @@
"""Tests for SMS integration."""

View File

@@ -1,143 +0,0 @@
"""Constants for tests of the SMS component."""
import datetime
SMS_STATUS_SINGLE = {
"SIMUnRead": 0,
"SIMUsed": 1,
"SIMSize": 30,
"PhoneUnRead": 0,
"PhoneUsed": 0,
"PhoneSize": 50,
"TemplatesUsed": 0,
}
NEXT_SMS_SINGLE = [
{
"SMSC": {
"Location": 0,
"Name": "",
"Format": "Text",
"Validity": "NA",
"Number": "+358444111111",
"DefaultNumber": "",
},
"UDH": {
"Type": "NoUDH",
"Text": b"",
"ID8bit": 0,
"ID16bit": 0,
"PartNumber": -1,
"AllParts": 0,
},
"Folder": 1,
"InboxFolder": 1,
"Memory": "SM",
"Location": 1,
"Name": "",
"Number": "+358444222222",
"Text": "Short message",
"Type": "Deliver",
"Coding": "Default_No_Compression",
"DateTime": datetime.datetime(2024, 3, 23, 20, 15, 37),
"SMSCDateTime": datetime.datetime(2024, 3, 23, 20, 15, 41),
"DeliveryStatus": 0,
"ReplyViaSameSMSC": 0,
"State": "UnRead",
"Class": -1,
"MessageReference": 0,
"ReplaceMessage": 0,
"RejectDuplicates": 0,
"Length": 7,
}
]
SMS_STATUS_MULTIPLE = {
"SIMUnRead": 0,
"SIMUsed": 2,
"SIMSize": 30,
"PhoneUnRead": 0,
"PhoneUsed": 0,
"PhoneSize": 50,
"TemplatesUsed": 0,
}
NEXT_SMS_MULTIPLE_1 = [
{
"SMSC": {
"Location": 0,
"Name": "",
"Format": "Text",
"Validity": "NA",
"Number": "+358444111111",
"DefaultNumber": "",
},
"UDH": {
"Type": "ConcatenatedMessages",
"Text": b"\x05\x00\x03\x00\x02\x01",
"ID8bit": 0,
"ID16bit": -1,
"PartNumber": 1,
"AllParts": 2,
},
"Folder": 1,
"InboxFolder": 1,
"Memory": "SM",
"Location": 1,
"Name": "",
"Number": "+358444222222",
"Text": "Longer test again: 01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123",
"Type": "Deliver",
"Coding": "Default_No_Compression",
"DateTime": datetime.datetime(2024, 3, 25, 19, 53, 56),
"SMSCDateTime": datetime.datetime(2024, 3, 25, 19, 54, 6),
"DeliveryStatus": 0,
"ReplyViaSameSMSC": 0,
"State": "UnRead",
"Class": -1,
"MessageReference": 0,
"ReplaceMessage": 0,
"RejectDuplicates": 0,
"Length": 153,
}
]
NEXT_SMS_MULTIPLE_2 = [
{
"SMSC": {
"Location": 0,
"Name": "",
"Format": "Text",
"Validity": "NA",
"Number": "+358444111111",
"DefaultNumber": "",
},
"UDH": {
"Type": "ConcatenatedMessages",
"Text": b"\x05\x00\x03\x00\x02\x02",
"ID8bit": 0,
"ID16bit": -1,
"PartNumber": 2,
"AllParts": 2,
},
"Folder": 1,
"InboxFolder": 1,
"Memory": "SM",
"Location": 2,
"Name": "",
"Number": "+358444222222",
"Text": "4567890123456789012345678901",
"Type": "Deliver",
"Coding": "Default_No_Compression",
"DateTime": datetime.datetime(2024, 3, 25, 19, 53, 56),
"SMSCDateTime": datetime.datetime(2024, 3, 25, 19, 54, 7),
"DeliveryStatus": 0,
"ReplyViaSameSMSC": 0,
"State": "UnRead",
"Class": -1,
"MessageReference": 0,
"ReplaceMessage": 0,
"RejectDuplicates": 0,
"Length": 28,
}
]

View File

@@ -1,52 +0,0 @@
"""Test the SMS Gateway."""
from unittest.mock import MagicMock
from homeassistant.components.sms.gateway import Gateway
from homeassistant.core import HomeAssistant
from .const import (
NEXT_SMS_MULTIPLE_1,
NEXT_SMS_MULTIPLE_2,
NEXT_SMS_SINGLE,
SMS_STATUS_MULTIPLE,
SMS_STATUS_SINGLE,
)
async def test_get_and_delete_all_sms_single_message(hass: HomeAssistant) -> None:
"""Test that a single message produces a list of entries containing the single message."""
# Mock the Gammu state_machine
state_machine = MagicMock()
state_machine.GetSMSStatus = MagicMock(return_value=SMS_STATUS_SINGLE)
state_machine.GetNextSMS = MagicMock(return_value=NEXT_SMS_SINGLE)
state_machine.DeleteSMS = MagicMock()
response = Gateway({"Connection": None}, hass).get_and_delete_all_sms(state_machine)
# Assert the length of the list
assert len(response) == 1
assert len(response[0]) == 1
# Assert the content of the message
assert response[0][0]["Text"] == "Short message"
async def test_get_and_delete_all_sms_two_part_message(hass: HomeAssistant) -> None:
"""Test that a two-part message produces a list of entries containing one combined message."""
state_machine = MagicMock()
state_machine.GetSMSStatus = MagicMock(return_value=SMS_STATUS_MULTIPLE)
state_machine.GetNextSMS = MagicMock(
side_effect=iter([NEXT_SMS_MULTIPLE_1, NEXT_SMS_MULTIPLE_2])
)
state_machine.DeleteSMS = MagicMock()
response = Gateway({"Connection": None}, hass).get_and_delete_all_sms(state_machine)
assert len(response) == 1
assert len(response[0]) == 2
assert response[0][0]["Text"] == NEXT_SMS_MULTIPLE_1[0]["Text"]
assert response[0][1]["Text"] == NEXT_SMS_MULTIPLE_2[0]["Text"]

View File

@@ -1,59 +0,0 @@
"""Test init."""
from unittest.mock import Mock, patch
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_DEVICE
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.helpers import issue_registry as ir
from tests.common import MockConfigEntry
@patch.dict(
"sys.modules",
{
"gammu": Mock(),
"gammu.asyncworker": Mock(),
},
)
async def test_repair_issue_is_created(
hass: HomeAssistant,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test repair issue is created."""
from homeassistant.components.sms import ( # noqa: PLC0415
DEPRECATED_ISSUE_ID,
DOMAIN,
)
with (
patch("homeassistant.components.sms.create_sms_gateway", autospec=True),
patch("homeassistant.components.sms.PLATFORMS", []),
):
config_entry = MockConfigEntry(
title="test",
domain=DOMAIN,
data={
CONF_DEVICE: "/dev/ttyUSB0",
},
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert (
HOMEASSISTANT_DOMAIN,
DEPRECATED_ISSUE_ID,
) in issue_registry.issues
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert (
HOMEASSISTANT_DOMAIN,
DEPRECATED_ISSUE_ID,
) not in issue_registry.issues